mirror of https://github.com/sm64pc/sm64pc.git
[WIP] Added multilanguage support & removed useless libraries
This commit is contained in:
parent
89d89b1121
commit
3096e20fd6
|
@ -2,7 +2,9 @@
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include "dir_utils.h"
|
#include "io_utils.h"
|
||||||
|
|
||||||
|
#define _READFILE_GUESS 256
|
||||||
|
|
||||||
void combine(char* destination, const char* path1, const char* path2) {
|
void combine(char* destination, const char* path1, const char* path2) {
|
||||||
if(path1 == NULL || path2 == NULL) {
|
if(path1 == NULL || path2 == NULL) {
|
||||||
|
@ -32,3 +34,44 @@ void combine(char* destination, const char* path1, const char* path2) {
|
||||||
strcat(destination, path2);
|
strcat(destination, path2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char *get_filename_ext(const char *filename) {
|
||||||
|
const char *dot = strrchr(filename, '.');
|
||||||
|
if(!dot || dot == filename) return "";
|
||||||
|
return dot + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* read_file(char* name){
|
||||||
|
FILE* file;
|
||||||
|
file = fopen(name, "r");
|
||||||
|
|
||||||
|
if(!file)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
char* result = malloc(sizeof(char) * _READFILE_GUESS + 1);
|
||||||
|
|
||||||
|
if(result == NULL)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
size_t pos = 0;
|
||||||
|
size_t capacity = _READFILE_GUESS;
|
||||||
|
char ch;
|
||||||
|
|
||||||
|
while((ch = getc(file)) != EOF){
|
||||||
|
result[pos++] = ch;
|
||||||
|
|
||||||
|
if(pos >= capacity){
|
||||||
|
capacity += _READFILE_GUESS;
|
||||||
|
result = realloc(result, sizeof(char) * capacity + 1);
|
||||||
|
if(result == NULL)
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(file);
|
||||||
|
result = realloc(result, sizeof(char) * pos);
|
||||||
|
if(result == NULL)
|
||||||
|
return NULL;
|
||||||
|
result[pos] = '\0';
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -2,5 +2,5 @@
|
||||||
#define DIRUTILS
|
#define DIRUTILS
|
||||||
|
|
||||||
extern void combine(char* destination, const char* path1, const char* path2);
|
extern void combine(char* destination, const char* path1, const char* path2);
|
||||||
|
extern const char *get_filename_ext(const char *filename);
|
||||||
#endif
|
#endif
|
|
@ -1,85 +1,49 @@
|
||||||
#include "text-loader.h"
|
#include "text-loader.h"
|
||||||
#include "txtconv.h"
|
#include "txtconv.h"
|
||||||
#include "libs/cJSON.h"
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "dialog_ids.h"
|
#include "dialog_ids.h"
|
||||||
#include "game/ingame_menu.h"
|
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include "libs/dir_utils.h"
|
#include "libs/io_utils.h"
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include "libs/cJSON.h"
|
||||||
|
|
||||||
struct DialogEntry* * dialogPool;
|
struct DialogEntry* * dialogPool;
|
||||||
#define _READFILE_GUESS 256
|
struct LanguageEntry* * languages;
|
||||||
|
s8 languagesAmount = 0;
|
||||||
|
|
||||||
#define SPANISH "./res/texts/es.json"
|
char* currentLanguage = "english";
|
||||||
#define ENGLISH "./res/texts/us.json"
|
|
||||||
|
|
||||||
char* read_file(char* name){
|
void preloadLanguageText(char* jsonTxt, s8 language){
|
||||||
FILE* file;
|
languages[language] = malloc (sizeof (struct LanguageEntry));
|
||||||
file = fopen(name, "r");
|
|
||||||
|
|
||||||
if(!file)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
char* result = malloc(sizeof(char) * _READFILE_GUESS + 1);
|
|
||||||
|
|
||||||
if(result == NULL)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
size_t pos = 0;
|
|
||||||
size_t capacity = _READFILE_GUESS;
|
|
||||||
char ch;
|
|
||||||
|
|
||||||
while((ch = getc(file)) != EOF){
|
|
||||||
result[pos++] = ch;
|
|
||||||
|
|
||||||
if(pos >= capacity){
|
|
||||||
capacity += _READFILE_GUESS;
|
|
||||||
result = realloc(result, sizeof(char) * capacity + 1);
|
|
||||||
if(result == NULL)
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fclose(file);
|
|
||||||
result = realloc(result, sizeof(char) * pos);
|
|
||||||
if(result == NULL)
|
|
||||||
return NULL;
|
|
||||||
result[pos] = '\0';
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void preloadTexts(){
|
|
||||||
|
|
||||||
char * file = SPANISH;
|
|
||||||
|
|
||||||
#ifndef WIN32
|
|
||||||
char * language_file = realpath(file, NULL);
|
|
||||||
#else
|
|
||||||
char * language_file = malloc(_MAX_PATH * sizeof(char));
|
|
||||||
_fullpath(language_file, file, _MAX_PATH );
|
|
||||||
#endif
|
|
||||||
|
|
||||||
printf("Loading File: %s\n", language_file);
|
|
||||||
|
|
||||||
char * jsonTxt = read_file(language_file);
|
|
||||||
|
|
||||||
cJSON *json = cJSON_Parse(jsonTxt);
|
cJSON *json = cJSON_Parse(jsonTxt);
|
||||||
|
|
||||||
if (json == NULL) {
|
if (json == NULL) {
|
||||||
const char *error_ptr = cJSON_GetErrorPtr();
|
const char *error_ptr = cJSON_GetErrorPtr();
|
||||||
|
|
||||||
if (error_ptr != NULL) {
|
if (error_ptr != NULL) {
|
||||||
fprintf(stderr, "Error before: %s\n", error_ptr);
|
fprintf(stderr, "Error before: %s\n", error_ptr);
|
||||||
}else{
|
} else {
|
||||||
fprintf(stderr, "Error loading the JSON file\n");
|
fprintf(stderr, "Error loading the JSON file\n");
|
||||||
}
|
}
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cJSON *dialog = NULL;
|
const cJSON *dialog = NULL;
|
||||||
const cJSON *dialogs = NULL;
|
const cJSON *dialogs = NULL;
|
||||||
|
const cJSON *manifest = cJSON_GetObjectItemCaseSensitive(json, "manifest");
|
||||||
|
|
||||||
|
struct DialogEntry** entries = malloc(DIALOG_COUNT * sizeof(struct DialogEntry));
|
||||||
|
|
||||||
|
languages[language]->name = cJSON_GetObjectItemCaseSensitive(manifest, "languageName")->valuestring;
|
||||||
|
languages[language]->logo = cJSON_GetObjectItemCaseSensitive(manifest, "languageLogo")->valuestring;
|
||||||
|
languages[language]->placeholder = cJSON_GetObjectItemCaseSensitive(manifest, "placeholder")->valuestring;
|
||||||
|
|
||||||
dialogs = cJSON_GetObjectItemCaseSensitive(json, "dialogs");
|
dialogs = cJSON_GetObjectItemCaseSensitive(json, "dialogs");
|
||||||
|
|
||||||
|
int eid = 0;
|
||||||
cJSON_ArrayForEach(dialog, dialogs) {
|
cJSON_ArrayForEach(dialog, dialogs) {
|
||||||
int id = cJSON_GetObjectItemCaseSensitive(dialog, "ID")->valueint;
|
int id = cJSON_GetObjectItemCaseSensitive(dialog, "ID")->valueint;
|
||||||
|
|
||||||
|
@ -109,15 +73,77 @@ void preloadTexts(){
|
||||||
}
|
}
|
||||||
|
|
||||||
entry->str = getTranslatedText(dialogTxt);
|
entry->str = getTranslatedText(dialogTxt);
|
||||||
dialogPool[id] = entry;
|
entries[eid] = entry;
|
||||||
|
eid++;
|
||||||
free(dialogTxt);
|
free(dialogTxt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
languages[language]->entries = entries;
|
||||||
|
|
||||||
cJSON_Delete(json);
|
cJSON_Delete(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void alloc_languages(void){
|
||||||
|
languages = realloc(languages, sizeof(struct LanguageEntry*) * 30);
|
||||||
|
|
||||||
|
char * languagesDir = "./res/texts/";
|
||||||
|
DIR *lf = opendir(languagesDir);
|
||||||
|
struct dirent *de;
|
||||||
|
while ((de = readdir(lf)) != NULL){
|
||||||
|
const char* extension = get_filename_ext(de->d_name);
|
||||||
|
char * file = malloc(99 * sizeof(char*));
|
||||||
|
if(strcmp(extension, "json") == 0){
|
||||||
|
strcpy(file, "");
|
||||||
|
strcat(file, "./res/texts/");
|
||||||
|
strcat(file, de->d_name);
|
||||||
|
|
||||||
|
#ifndef WIN32
|
||||||
|
char * language_file = realpath(file, NULL);
|
||||||
|
#else
|
||||||
|
char * language_file = malloc(_MAX_PATH * sizeof(char));
|
||||||
|
_fullpath(language_file, file, _MAX_PATH );
|
||||||
|
#endif
|
||||||
|
|
||||||
|
printf("Loading File: %s\n", language_file);
|
||||||
|
languagesAmount++;
|
||||||
|
|
||||||
|
char * jsonTxt = read_file(language_file);
|
||||||
|
preloadLanguageText(jsonTxt, languagesAmount - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
languages = realloc(languages, sizeof(struct LanguageEntry*) * (languagesAmount));
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectLanguage(char* languageName){
|
||||||
|
|
||||||
|
char* lowerName = malloc(sizeof(languageName));
|
||||||
|
for(char l = 0; l < sizeof(lowerName) / sizeof(char); l++)
|
||||||
|
lowerName[l] = tolower(languageName[l]);
|
||||||
|
|
||||||
|
int id = 0;
|
||||||
|
char* languageTmp = "none";
|
||||||
|
|
||||||
|
for(int l = 0; l < languagesAmount; l++){
|
||||||
|
if(strcmp(languages[l]->name, lowerName) == 0){
|
||||||
|
id = l;
|
||||||
|
languageTmp = languages[l]->name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLanguage = languageTmp;
|
||||||
|
dialogPool = languages[id]->entries;
|
||||||
|
}
|
||||||
|
|
||||||
void alloc_dialog_pool(void){
|
void alloc_dialog_pool(void){
|
||||||
dialogPool = malloc(DIALOG_COUNT * sizeof(struct DialogEntry));
|
languages = malloc(sizeof(struct LanguageEntry*));
|
||||||
|
|
||||||
|
languages[0] = malloc (sizeof (struct LanguageEntry));
|
||||||
|
languages[0]->name = "none";
|
||||||
|
languages[0]->logo = "none";
|
||||||
|
languages[0]->placeholder = "You are not supposed\nto be here.\n\nKeep this as a secret\n\n- Render96 Team";
|
||||||
|
languages[0]->entries = malloc(DIALOG_COUNT * sizeof(struct DialogEntry));
|
||||||
|
|
||||||
for(int i = 0; i < DIALOG_COUNT; i++){
|
for(int i = 0; i < DIALOG_COUNT; i++){
|
||||||
struct DialogEntry *entry = malloc (sizeof (struct DialogEntry));
|
struct DialogEntry *entry = malloc (sizeof (struct DialogEntry));
|
||||||
|
@ -126,9 +152,11 @@ void alloc_dialog_pool(void){
|
||||||
entry->linesPerBox = 6;
|
entry->linesPerBox = 6;
|
||||||
entry->leftOffset = 95;
|
entry->leftOffset = 95;
|
||||||
entry->width = 200;
|
entry->width = 200;
|
||||||
entry->str = getTranslatedText("You are not supposed\nto be here.\n\nKeep this as a secret\n\n- Render96 Team");
|
entry->str = getTranslatedText(languages[0]->placeholder);
|
||||||
dialogPool[i] = entry;
|
|
||||||
|
languages[0]->entries[i] = entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
preloadTexts();
|
alloc_languages();
|
||||||
|
selectLanguage(currentLanguage);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,20 @@
|
||||||
#define TXTLOADER
|
#define TXTLOADER
|
||||||
|
|
||||||
#include "types.h"
|
#include "types.h"
|
||||||
|
#include "game/ingame_menu.h"
|
||||||
|
|
||||||
|
extern char* currentLanguage;
|
||||||
|
extern s8 languagesAmount;
|
||||||
|
|
||||||
extern struct DialogEntry ** dialogPool;
|
extern struct DialogEntry ** dialogPool;
|
||||||
|
extern char* read_file(char* name);
|
||||||
|
|
||||||
|
struct LanguageEntry {
|
||||||
|
char * name;
|
||||||
|
char * logo;
|
||||||
|
char * placeholder;
|
||||||
|
struct DialogEntry* * entries;
|
||||||
|
};
|
||||||
|
|
||||||
extern void alloc_dialog_pool(void);
|
extern void alloc_dialog_pool(void);
|
||||||
#endif
|
#endif
|
|
@ -1,4 +1,9 @@
|
||||||
{
|
{
|
||||||
|
"manifest": {
|
||||||
|
"languageName": "Spanish",
|
||||||
|
"languageLogo": "none",
|
||||||
|
"placeholder": "You are not supposed\nto be here.\n\nKeep this as a secret\n\n- Render96 Team"
|
||||||
|
},
|
||||||
"dialogs": [
|
"dialogs": [
|
||||||
{
|
{
|
||||||
"ID": 0,
|
"ID": 0,
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
{
|
{
|
||||||
|
"manifest": {
|
||||||
|
"languageName": "English",
|
||||||
|
"languageLogo": "none",
|
||||||
|
"placeholder": "You are not supposed\nto be here.\n\nKeep this as a secret\n\n- Render96 Team"
|
||||||
|
},
|
||||||
"dialogs": [
|
"dialogs": [
|
||||||
{
|
{
|
||||||
"ID": 0,
|
"ID": 0,
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# Given a list of header files, compute the bss index that results from
|
|
||||||
# including them. (See prevent_bss_reordering.h for more information.)
|
|
||||||
|
|
||||||
TEMPC=$(mktemp -t bss.XXXXXXX.c)
|
|
||||||
TEMPO=$(mktemp -t bss.XXXXXXX.o)
|
|
||||||
trap "rm -f $TEMPC $TEMPO" EXIT
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [[ $# = 0 ]]; then
|
|
||||||
echo "Usage: ./tools/calc_bss.sh file1.h file2.h ..." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$CROSS" ]; then
|
|
||||||
CROSS=mips-linux-gnu-
|
|
||||||
fi
|
|
||||||
|
|
||||||
# bss indexing starts at 3
|
|
||||||
for I in {3..255}; do
|
|
||||||
echo "char bss$I;" >> $TEMPC
|
|
||||||
done
|
|
||||||
for I in {0..2}; do
|
|
||||||
echo "char bss$I;" >> $TEMPC
|
|
||||||
done
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
echo "#include \"$1\"" >> $TEMPC
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "char measurement;" >> $TEMPC
|
|
||||||
|
|
||||||
LINE=$(${CROSS}objdump -t $TEMPO | grep measurement | cut -d' ' -f1)
|
|
||||||
NUM=$((0x$LINE - 1))
|
|
||||||
echo "bss index: $NUM"
|
|
575
tools/libmio0.c
575
tools/libmio0.c
|
@ -1,575 +0,0 @@
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#if defined(_WIN32) || defined(_WIN64)
|
|
||||||
#include <io.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "libmio0.h"
|
|
||||||
#include "utils.h"
|
|
||||||
|
|
||||||
// defines
|
|
||||||
|
|
||||||
#define MIO0_VERSION "0.1"
|
|
||||||
|
|
||||||
#define GET_BIT(buf, bit) ((buf)[(bit) / 8] & (1 << (7 - ((bit) % 8))))
|
|
||||||
|
|
||||||
// types
|
|
||||||
typedef struct
|
|
||||||
{
|
|
||||||
int *indexes;
|
|
||||||
int allocated;
|
|
||||||
int count;
|
|
||||||
int start;
|
|
||||||
} lookback;
|
|
||||||
|
|
||||||
// functions
|
|
||||||
#define LOOKBACK_COUNT 256
|
|
||||||
#define LOOKBACK_INIT_SIZE 128
|
|
||||||
static lookback *lookback_init(void)
|
|
||||||
{
|
|
||||||
lookback *lb = malloc(LOOKBACK_COUNT * sizeof(*lb));
|
|
||||||
for (int i = 0; i < LOOKBACK_COUNT; i++) {
|
|
||||||
lb[i].allocated = LOOKBACK_INIT_SIZE;
|
|
||||||
lb[i].indexes = malloc(lb[i].allocated * sizeof(*lb[i].indexes));
|
|
||||||
lb[i].count = 0;
|
|
||||||
lb[i].start = 0;
|
|
||||||
}
|
|
||||||
return lb;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void lookback_free(lookback *lb)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < LOOKBACK_COUNT; i++) {
|
|
||||||
free(lb[i].indexes);
|
|
||||||
}
|
|
||||||
free(lb);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void lookback_push(lookback *lkbk, unsigned char val, int index)
|
|
||||||
{
|
|
||||||
lookback *lb = &lkbk[val];
|
|
||||||
if (lb->count == lb->allocated) {
|
|
||||||
lb->allocated *= 4;
|
|
||||||
lb->indexes = realloc(lb->indexes, lb->allocated * sizeof(*lb->indexes));
|
|
||||||
}
|
|
||||||
lb->indexes[lb->count++] = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void PUT_BIT(unsigned char *buf, int bit, int val)
|
|
||||||
{
|
|
||||||
unsigned char mask = 1 << (7 - (bit % 8));
|
|
||||||
unsigned int offset = bit / 8;
|
|
||||||
buf[offset] = (buf[offset] & ~(mask)) | (val ? mask : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// used to find longest matching stream in buffer
|
|
||||||
// buf: buffer
|
|
||||||
// start_offset: offset in buf to look back from
|
|
||||||
// max_search: max number of bytes to find
|
|
||||||
// found_offset: returned offset found (0 if none found)
|
|
||||||
// returns max length of matching stream (0 if none found)
|
|
||||||
static int find_longest(const unsigned char *buf, int start_offset, int max_search, int *found_offset, lookback *lkbk)
|
|
||||||
{
|
|
||||||
int best_length = 0;
|
|
||||||
int best_offset = 0;
|
|
||||||
int cur_length;
|
|
||||||
int search_len;
|
|
||||||
int farthest, off, i;
|
|
||||||
int lb_idx;
|
|
||||||
const unsigned char first = buf[start_offset];
|
|
||||||
lookback *lb = &lkbk[first];
|
|
||||||
|
|
||||||
// buf
|
|
||||||
// | off start max
|
|
||||||
// V |+i-> |+i-> |
|
|
||||||
// |--------------raw-data-----------------|
|
|
||||||
// |+i-> | |+i->
|
|
||||||
// +cur_length
|
|
||||||
|
|
||||||
// check at most the past 4096 values
|
|
||||||
farthest = MAX(start_offset - 4096, 0);
|
|
||||||
// find starting index
|
|
||||||
for (lb_idx = lb->start; lb_idx < lb->count && lb->indexes[lb_idx] < farthest; lb_idx++) {}
|
|
||||||
lb->start = lb_idx;
|
|
||||||
for ( ; lb_idx < lb->count && lb->indexes[lb_idx] < start_offset; lb_idx++) {
|
|
||||||
off = lb->indexes[lb_idx];
|
|
||||||
// check at most requested max or up until start
|
|
||||||
search_len = MIN(max_search, start_offset - off);
|
|
||||||
for (i = 0; i < search_len; i++) {
|
|
||||||
if (buf[start_offset + i] != buf[off + i]) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cur_length = i;
|
|
||||||
// if matched up until start, continue matching in already matched parts
|
|
||||||
if (cur_length == search_len) {
|
|
||||||
// check at most requested max less current length
|
|
||||||
search_len = max_search - cur_length;
|
|
||||||
for (i = 0; i < search_len; i++) {
|
|
||||||
if (buf[start_offset + cur_length + i] != buf[off + i]) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cur_length += i;
|
|
||||||
}
|
|
||||||
if (cur_length > best_length) {
|
|
||||||
best_offset = start_offset - off;
|
|
||||||
best_length = cur_length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return best reverse offset and length (may be 0)
|
|
||||||
*found_offset = best_offset;
|
|
||||||
return best_length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// decode MIO0 header
|
|
||||||
// returns 1 if valid header, 0 otherwise
|
|
||||||
int mio0_decode_header(const unsigned char *buf, mio0_header_t *head)
|
|
||||||
{
|
|
||||||
if (!memcmp(buf, "MIO0", 4)) {
|
|
||||||
head->dest_size = read_u32_be(&buf[4]);
|
|
||||||
head->comp_offset = read_u32_be(&buf[8]);
|
|
||||||
head->uncomp_offset = read_u32_be(&buf[12]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void mio0_encode_header(unsigned char *buf, const mio0_header_t *head)
|
|
||||||
{
|
|
||||||
memcpy(buf, "MIO0", 4);
|
|
||||||
write_u32_be(&buf[4], head->dest_size);
|
|
||||||
write_u32_be(&buf[8], head->comp_offset);
|
|
||||||
write_u32_be(&buf[12], head->uncomp_offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
int mio0_decode(const unsigned char *in, unsigned char *out, unsigned int *end)
|
|
||||||
{
|
|
||||||
mio0_header_t head;
|
|
||||||
unsigned int bytes_written = 0;
|
|
||||||
int bit_idx = 0;
|
|
||||||
int comp_idx = 0;
|
|
||||||
int uncomp_idx = 0;
|
|
||||||
int valid;
|
|
||||||
|
|
||||||
// extract header
|
|
||||||
valid = mio0_decode_header(in, &head);
|
|
||||||
// verify MIO0 header
|
|
||||||
if (!valid) {
|
|
||||||
return -2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// decode data
|
|
||||||
while (bytes_written < head.dest_size) {
|
|
||||||
if (GET_BIT(&in[MIO0_HEADER_LENGTH], bit_idx)) {
|
|
||||||
// 1 - pull uncompressed data
|
|
||||||
out[bytes_written] = in[head.uncomp_offset + uncomp_idx];
|
|
||||||
bytes_written++;
|
|
||||||
uncomp_idx++;
|
|
||||||
} else {
|
|
||||||
// 0 - read compressed data
|
|
||||||
int idx;
|
|
||||||
int length;
|
|
||||||
int i;
|
|
||||||
const unsigned char *vals = &in[head.comp_offset + comp_idx];
|
|
||||||
comp_idx += 2;
|
|
||||||
length = ((vals[0] & 0xF0) >> 4) + 3;
|
|
||||||
idx = ((vals[0] & 0x0F) << 8) + vals[1] + 1;
|
|
||||||
for (i = 0; i < length; i++) {
|
|
||||||
out[bytes_written] = out[bytes_written - idx];
|
|
||||||
bytes_written++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bit_idx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end) {
|
|
||||||
*end = head.uncomp_offset + uncomp_idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytes_written;
|
|
||||||
}
|
|
||||||
|
|
||||||
int mio0_encode(const unsigned char *in, unsigned int length, unsigned char *out)
|
|
||||||
{
|
|
||||||
unsigned char *bit_buf;
|
|
||||||
unsigned char *comp_buf;
|
|
||||||
unsigned char *uncomp_buf;
|
|
||||||
unsigned int bit_length;
|
|
||||||
unsigned int comp_offset;
|
|
||||||
unsigned int uncomp_offset;
|
|
||||||
unsigned int bytes_proc = 0;
|
|
||||||
int bytes_written;
|
|
||||||
int bit_idx = 0;
|
|
||||||
int comp_idx = 0;
|
|
||||||
int uncomp_idx = 0;
|
|
||||||
lookback *lookbacks;
|
|
||||||
|
|
||||||
// initialize lookback buffer
|
|
||||||
lookbacks = lookback_init();
|
|
||||||
|
|
||||||
// allocate some temporary buffers worst case size
|
|
||||||
bit_buf = malloc((length + 7) / 8); // 1-bit/byte
|
|
||||||
comp_buf = malloc(length); // 16-bits/2bytes
|
|
||||||
uncomp_buf = malloc(length); // all uncompressed
|
|
||||||
memset(bit_buf, 0, (length + 7) / 8);
|
|
||||||
|
|
||||||
// encode data
|
|
||||||
// special case for first byte
|
|
||||||
lookback_push(lookbacks, in[0], 0);
|
|
||||||
uncomp_buf[uncomp_idx] = in[0];
|
|
||||||
uncomp_idx += 1;
|
|
||||||
bytes_proc += 1;
|
|
||||||
PUT_BIT(bit_buf, bit_idx++, 1);
|
|
||||||
while (bytes_proc < length) {
|
|
||||||
int offset;
|
|
||||||
int max_length = MIN(length - bytes_proc, 18);
|
|
||||||
int longest_match = find_longest(in, bytes_proc, max_length, &offset, lookbacks);
|
|
||||||
// push current byte before checking next longer match
|
|
||||||
lookback_push(lookbacks, in[bytes_proc], bytes_proc);
|
|
||||||
if (longest_match > 2) {
|
|
||||||
int lookahead_offset;
|
|
||||||
// lookahead to next byte to see if longer match
|
|
||||||
int lookahead_length = MIN(length - bytes_proc - 1, 18);
|
|
||||||
int lookahead_match = find_longest(in, bytes_proc + 1, lookahead_length, &lookahead_offset, lookbacks);
|
|
||||||
// better match found, use uncompressed + lookahead compressed
|
|
||||||
if ((longest_match + 1) < lookahead_match) {
|
|
||||||
// uncompressed byte
|
|
||||||
uncomp_buf[uncomp_idx] = in[bytes_proc];
|
|
||||||
uncomp_idx++;
|
|
||||||
PUT_BIT(bit_buf, bit_idx, 1);
|
|
||||||
bytes_proc++;
|
|
||||||
longest_match = lookahead_match;
|
|
||||||
offset = lookahead_offset;
|
|
||||||
bit_idx++;
|
|
||||||
lookback_push(lookbacks, in[bytes_proc], bytes_proc);
|
|
||||||
}
|
|
||||||
// first byte already pushed above
|
|
||||||
for (int i = 1; i < longest_match; i++) {
|
|
||||||
lookback_push(lookbacks, in[bytes_proc + i], bytes_proc + i);
|
|
||||||
}
|
|
||||||
// compressed block
|
|
||||||
comp_buf[comp_idx] = (((longest_match - 3) & 0x0F) << 4) |
|
|
||||||
(((offset - 1) >> 8) & 0x0F);
|
|
||||||
comp_buf[comp_idx + 1] = (offset - 1) & 0xFF;
|
|
||||||
comp_idx += 2;
|
|
||||||
PUT_BIT(bit_buf, bit_idx, 0);
|
|
||||||
bytes_proc += longest_match;
|
|
||||||
} else {
|
|
||||||
// uncompressed byte
|
|
||||||
uncomp_buf[uncomp_idx] = in[bytes_proc];
|
|
||||||
uncomp_idx++;
|
|
||||||
PUT_BIT(bit_buf, bit_idx, 1);
|
|
||||||
bytes_proc++;
|
|
||||||
}
|
|
||||||
bit_idx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// compute final sizes and offsets
|
|
||||||
// +7 so int division accounts for all bits
|
|
||||||
bit_length = ((bit_idx + 7) / 8);
|
|
||||||
// compressed data after control bits and aligned to 4-byte boundary
|
|
||||||
comp_offset = ALIGN(MIO0_HEADER_LENGTH + bit_length, 4);
|
|
||||||
uncomp_offset = comp_offset + comp_idx;
|
|
||||||
bytes_written = uncomp_offset + uncomp_idx;
|
|
||||||
|
|
||||||
// output header
|
|
||||||
memcpy(out, "MIO0", 4);
|
|
||||||
write_u32_be(&out[4], length);
|
|
||||||
write_u32_be(&out[8], comp_offset);
|
|
||||||
write_u32_be(&out[12], uncomp_offset);
|
|
||||||
// output data
|
|
||||||
memcpy(&out[MIO0_HEADER_LENGTH], bit_buf, bit_length);
|
|
||||||
memcpy(&out[comp_offset], comp_buf, comp_idx);
|
|
||||||
memcpy(&out[uncomp_offset], uncomp_buf, uncomp_idx);
|
|
||||||
|
|
||||||
// free allocated buffers
|
|
||||||
free(bit_buf);
|
|
||||||
free(comp_buf);
|
|
||||||
free(uncomp_buf);
|
|
||||||
lookback_free(lookbacks);
|
|
||||||
|
|
||||||
return bytes_written;
|
|
||||||
}
|
|
||||||
|
|
||||||
static FILE *mio0_open_out_file(const char *out_file) {
|
|
||||||
if (strcmp(out_file, "-") == 0) {
|
|
||||||
#if defined(_WIN32) || defined(_WIN64)
|
|
||||||
_setmode(_fileno(stdout), _O_BINARY);
|
|
||||||
#endif
|
|
||||||
return stdout;
|
|
||||||
} else {
|
|
||||||
return fopen(out_file, "wb");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int mio0_decode_file(const char *in_file, unsigned long offset, const char *out_file)
|
|
||||||
{
|
|
||||||
mio0_header_t head;
|
|
||||||
FILE *in;
|
|
||||||
FILE *out;
|
|
||||||
unsigned char *in_buf = NULL;
|
|
||||||
unsigned char *out_buf = NULL;
|
|
||||||
long file_size;
|
|
||||||
int ret_val = 0;
|
|
||||||
size_t bytes_read;
|
|
||||||
int bytes_decoded;
|
|
||||||
int bytes_written;
|
|
||||||
int valid;
|
|
||||||
|
|
||||||
in = fopen(in_file, "rb");
|
|
||||||
if (in == NULL) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// allocate buffer to read from offset to end of file
|
|
||||||
fseek(in, 0, SEEK_END);
|
|
||||||
file_size = ftell(in);
|
|
||||||
in_buf = malloc(file_size - offset);
|
|
||||||
fseek(in, offset, SEEK_SET);
|
|
||||||
|
|
||||||
// read bytes
|
|
||||||
bytes_read = fread(in_buf, 1, file_size - offset, in);
|
|
||||||
if (bytes_read != file_size - offset) {
|
|
||||||
ret_val = 2;
|
|
||||||
goto free_all;
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify header
|
|
||||||
valid = mio0_decode_header(in_buf, &head);
|
|
||||||
if (!valid) {
|
|
||||||
ret_val = 3;
|
|
||||||
goto free_all;
|
|
||||||
}
|
|
||||||
out_buf = malloc(head.dest_size);
|
|
||||||
|
|
||||||
// decompress MIO0 encoded data
|
|
||||||
bytes_decoded = mio0_decode(in_buf, out_buf, NULL);
|
|
||||||
if (bytes_decoded < 0) {
|
|
||||||
ret_val = 3;
|
|
||||||
goto free_all;
|
|
||||||
}
|
|
||||||
|
|
||||||
// open output file
|
|
||||||
out = mio0_open_out_file(out_file);
|
|
||||||
if (out == NULL) {
|
|
||||||
ret_val = 4;
|
|
||||||
goto free_all;
|
|
||||||
}
|
|
||||||
|
|
||||||
// write data to file
|
|
||||||
bytes_written = fwrite(out_buf, 1, bytes_decoded, out);
|
|
||||||
if (bytes_written != bytes_decoded) {
|
|
||||||
ret_val = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
// clean up
|
|
||||||
if (out != stdout) {
|
|
||||||
fclose(out);
|
|
||||||
}
|
|
||||||
free_all:
|
|
||||||
if (out_buf) {
|
|
||||||
free(out_buf);
|
|
||||||
}
|
|
||||||
if (in_buf) {
|
|
||||||
free(in_buf);
|
|
||||||
}
|
|
||||||
fclose(in);
|
|
||||||
|
|
||||||
return ret_val;
|
|
||||||
}
|
|
||||||
|
|
||||||
int mio0_encode_file(const char *in_file, const char *out_file)
|
|
||||||
{
|
|
||||||
FILE *in;
|
|
||||||
FILE *out;
|
|
||||||
unsigned char *in_buf = NULL;
|
|
||||||
unsigned char *out_buf = NULL;
|
|
||||||
size_t file_size;
|
|
||||||
size_t bytes_read;
|
|
||||||
int bytes_encoded;
|
|
||||||
int bytes_written;
|
|
||||||
int ret_val = 0;
|
|
||||||
|
|
||||||
in = fopen(in_file, "rb");
|
|
||||||
if (in == NULL) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// allocate buffer to read entire contents of files
|
|
||||||
fseek(in, 0, SEEK_END);
|
|
||||||
file_size = ftell(in);
|
|
||||||
fseek(in, 0, SEEK_SET);
|
|
||||||
in_buf = malloc(file_size);
|
|
||||||
|
|
||||||
// read bytes
|
|
||||||
bytes_read = fread(in_buf, 1, file_size, in);
|
|
||||||
if (bytes_read != file_size) {
|
|
||||||
ret_val = 2;
|
|
||||||
goto free_all;
|
|
||||||
}
|
|
||||||
|
|
||||||
// allocate worst case length
|
|
||||||
out_buf = malloc(MIO0_HEADER_LENGTH + ((file_size+7)/8) + file_size);
|
|
||||||
|
|
||||||
// compress data in MIO0 format
|
|
||||||
bytes_encoded = mio0_encode(in_buf, file_size, out_buf);
|
|
||||||
|
|
||||||
// open output file
|
|
||||||
out = mio0_open_out_file(out_file);
|
|
||||||
if (out == NULL) {
|
|
||||||
ret_val = 4;
|
|
||||||
goto free_all;
|
|
||||||
}
|
|
||||||
|
|
||||||
// write data to file
|
|
||||||
bytes_written = fwrite(out_buf, 1, bytes_encoded, out);
|
|
||||||
if (bytes_written != bytes_encoded) {
|
|
||||||
ret_val = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
// clean up
|
|
||||||
if (out != stdout) {
|
|
||||||
fclose(out);
|
|
||||||
}
|
|
||||||
free_all:
|
|
||||||
if (out_buf) {
|
|
||||||
free(out_buf);
|
|
||||||
}
|
|
||||||
if (in_buf) {
|
|
||||||
free(in_buf);
|
|
||||||
}
|
|
||||||
fclose(in);
|
|
||||||
|
|
||||||
return ret_val;
|
|
||||||
}
|
|
||||||
|
|
||||||
// mio0 standalone executable
|
|
||||||
#ifdef MIO0_STANDALONE
|
|
||||||
typedef struct
|
|
||||||
{
|
|
||||||
char *in_filename;
|
|
||||||
char *out_filename;
|
|
||||||
unsigned int offset;
|
|
||||||
int compress;
|
|
||||||
} arg_config;
|
|
||||||
|
|
||||||
static arg_config default_config =
|
|
||||||
{
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
0,
|
|
||||||
1
|
|
||||||
};
|
|
||||||
|
|
||||||
static void print_usage(void)
|
|
||||||
{
|
|
||||||
ERROR("Usage: mio0 [-c / -d] [-o OFFSET] FILE [OUTPUT]\n"
|
|
||||||
"\n"
|
|
||||||
"mio0 v" MIO0_VERSION ": MIO0 compression and decompression tool\n"
|
|
||||||
"\n"
|
|
||||||
"Optional arguments:\n"
|
|
||||||
" -c compress raw data into MIO0 (default: compress)\n"
|
|
||||||
" -d decompress MIO0 into raw data\n"
|
|
||||||
" -o OFFSET starting offset in FILE (default: 0)\n"
|
|
||||||
"\n"
|
|
||||||
"File arguments:\n"
|
|
||||||
" FILE input file\n"
|
|
||||||
" [OUTPUT] output file (default: FILE.out), \"-\" for stdout\n");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse command line arguments
|
|
||||||
static void parse_arguments(int argc, char *argv[], arg_config *config)
|
|
||||||
{
|
|
||||||
int i;
|
|
||||||
int file_count = 0;
|
|
||||||
if (argc < 2) {
|
|
||||||
print_usage();
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
for (i = 1; i < argc; i++) {
|
|
||||||
if (argv[i][0] == '-' && argv[i][1] != '\0') {
|
|
||||||
switch (argv[i][1]) {
|
|
||||||
case 'c':
|
|
||||||
config->compress = 1;
|
|
||||||
break;
|
|
||||||
case 'd':
|
|
||||||
config->compress = 0;
|
|
||||||
break;
|
|
||||||
case 'o':
|
|
||||||
if (++i >= argc) {
|
|
||||||
print_usage();
|
|
||||||
}
|
|
||||||
config->offset = strtoul(argv[i], NULL, 0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
print_usage();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (file_count) {
|
|
||||||
case 0:
|
|
||||||
config->in_filename = argv[i];
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
config->out_filename = argv[i];
|
|
||||||
break;
|
|
||||||
default: // too many
|
|
||||||
print_usage();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
file_count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (file_count < 1) {
|
|
||||||
print_usage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
|
||||||
{
|
|
||||||
char out_filename[FILENAME_MAX];
|
|
||||||
arg_config config;
|
|
||||||
int ret_val;
|
|
||||||
|
|
||||||
// get configuration from arguments
|
|
||||||
config = default_config;
|
|
||||||
parse_arguments(argc, argv, &config);
|
|
||||||
if (config.out_filename == NULL) {
|
|
||||||
config.out_filename = out_filename;
|
|
||||||
sprintf(config.out_filename, "%s.out", config.in_filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
// operation
|
|
||||||
if (config.compress) {
|
|
||||||
ret_val = mio0_encode_file(config.in_filename, config.out_filename);
|
|
||||||
} else {
|
|
||||||
ret_val = mio0_decode_file(config.in_filename, config.offset, config.out_filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (ret_val) {
|
|
||||||
case 1:
|
|
||||||
ERROR("Error opening input file \"%s\"\n", config.in_filename);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
ERROR("Error reading from input file \"%s\"\n", config.in_filename);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
ERROR("Error decoding MIO0 data. Wrong offset (0x%X)?\n", config.offset);
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
ERROR("Error opening output file \"%s\"\n", config.out_filename);
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
ERROR("Error writing bytes to output file \"%s\"\n", config.out_filename);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret_val;
|
|
||||||
}
|
|
||||||
#endif // MIO0_STANDALONE
|
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
#ifndef LIBMIO0_H_
|
|
||||||
#define LIBMIO0_H_
|
|
||||||
|
|
||||||
// defines
|
|
||||||
|
|
||||||
#define MIO0_HEADER_LENGTH 16
|
|
||||||
|
|
||||||
// typedefs
|
|
||||||
|
|
||||||
typedef struct
|
|
||||||
{
|
|
||||||
unsigned int dest_size;
|
|
||||||
unsigned int comp_offset;
|
|
||||||
unsigned int uncomp_offset;
|
|
||||||
} mio0_header_t;
|
|
||||||
|
|
||||||
// function prototypes
|
|
||||||
|
|
||||||
// decode MIO0 header
|
|
||||||
// returns 1 if valid header, 0 otherwise
|
|
||||||
int mio0_decode_header(const unsigned char *buf, mio0_header_t *head);
|
|
||||||
|
|
||||||
// encode MIO0 header from struct
|
|
||||||
void mio0_encode_header(unsigned char *buf, const mio0_header_t *head);
|
|
||||||
|
|
||||||
// decode MIO0 data in memory
|
|
||||||
// in: buffer containing MIO0 data
|
|
||||||
// out: buffer for output data
|
|
||||||
// end: output offset of the last byte decoded from in (set to NULL if unwanted)
|
|
||||||
// returns bytes extracted to 'out' or negative value on failure
|
|
||||||
int mio0_decode(const unsigned char *in, unsigned char *out, unsigned int *end);
|
|
||||||
|
|
||||||
// encode MIO0 data in memory
|
|
||||||
// in: buffer containing raw data
|
|
||||||
// out: buffer for MIO0 data
|
|
||||||
// returns size of compressed data in 'out' including MIO0 header
|
|
||||||
int mio0_encode(const unsigned char *in, unsigned int length, unsigned char *out);
|
|
||||||
|
|
||||||
// decode an entire MIO0 block at an offset from file to output file
|
|
||||||
// in_file: input filename
|
|
||||||
// offset: offset to start decoding from in_file
|
|
||||||
// out_file: output filename
|
|
||||||
int mio0_decode_file(const char *in_file, unsigned long offset, const char *out_file);
|
|
||||||
|
|
||||||
// encode an entire file
|
|
||||||
// in_file: input filename containing raw data to be encoded
|
|
||||||
// out_file: output filename to write MIO0 compressed data to
|
|
||||||
int mio0_encode_file(const char *in_file, const char *out_file);
|
|
||||||
|
|
||||||
#endif // LIBMIO0_H_
|
|
|
@ -1,58 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Patches the malloc() function in libmalloc.so to allocate more than the
|
|
||||||
# specified number of bytes. This is needed to work around issues with the
|
|
||||||
# compiler occasionally crashing.
|
|
||||||
#
|
|
||||||
# This script replaces the "move a1, a0" (00 80 28 25) instruction with
|
|
||||||
# "addiu a1, a0, n" (24 85 nn nn), which causes the malloc function to add n to
|
|
||||||
# the size parameter that was passed in.
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import os.path
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# file to patch
|
|
||||||
filename = 'tools/ido5.3_compiler/lib/libmalloc.so'
|
|
||||||
# Expected (unpatched) hash of file
|
|
||||||
filehash = 'adde672b5d79b52ca3cce9a47c7cb648'
|
|
||||||
# location in file to patch
|
|
||||||
address = 0xAB4
|
|
||||||
|
|
||||||
# Get parameter
|
|
||||||
if len(sys.argv) != 2:
|
|
||||||
print('Usage: ' + sys.argv[0] + ' n\n where n is the number of extra bytes to allocate in malloc()')
|
|
||||||
exit(1)
|
|
||||||
n = int(sys.argv[1])
|
|
||||||
|
|
||||||
# Original instruction "move a1, a0"
|
|
||||||
oldinsn = bytearray([0x00, 0x80, 0x28, 0x25])
|
|
||||||
|
|
||||||
# New instruction "addiu a1, a0, n"
|
|
||||||
newinsn = bytearray([0x24, 0x85, (n >> 8) & 0xFF, (n & 0xFF)])
|
|
||||||
|
|
||||||
# Patch the file
|
|
||||||
try:
|
|
||||||
with open(filename, 'rb+') as f:
|
|
||||||
# Read file contents
|
|
||||||
contents = bytearray(f.read())
|
|
||||||
|
|
||||||
# Unpatch the file by restoring original instruction
|
|
||||||
contents[address:address+4] = oldinsn
|
|
||||||
|
|
||||||
# Verify the (unpatched) hash of the file
|
|
||||||
md5 = hashlib.md5()
|
|
||||||
md5.update(contents)
|
|
||||||
if md5.hexdigest() != filehash:
|
|
||||||
print('Error: ' + filename + ' does not appear to be the correct version.')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
# Patch the file
|
|
||||||
if n != 0:
|
|
||||||
contents[address:address+4] = newinsn
|
|
||||||
|
|
||||||
# Write file
|
|
||||||
f.seek(0, os.SEEK_SET)
|
|
||||||
f.write(contents)
|
|
||||||
except IOError as e:
|
|
||||||
print('Error: Could not open library file for writing: ' + str(e))
|
|
Loading…
Reference in New Issue