mirror of
https://github.com/marqs85/ossc.git
synced 2024-12-27 11:30:11 +00:00
Merge branch 'megari-release_userdata_export_fat16' into release
This commit is contained in:
commit
20ac572baf
@ -159,12 +159,17 @@ C_SRCS += ossc/av_controller.c
|
||||
C_SRCS += ossc/avconfig.c
|
||||
C_SRCS += ossc/controls.c
|
||||
C_SRCS += ossc/firmware.c
|
||||
C_SRCS += ossc/fat16_export.c
|
||||
ifeq ($(OSDLANG),JP)
|
||||
C_SRCS += ossc/menu_sjis.c
|
||||
else
|
||||
C_SRCS += ossc/menu.c
|
||||
endif
|
||||
ifeq ($(OSDLANG),JP)
|
||||
C_SRCS += ossc/userdata_sjis.c
|
||||
else
|
||||
C_SRCS += ossc/userdata.c
|
||||
endif
|
||||
C_SRCS += ossc/utils.c
|
||||
C_SRCS += ulibSD/sd_io.c
|
||||
C_SRCS += ulibSD/spi_io.c
|
||||
@ -970,7 +975,7 @@ clean : clean_elf_derived_files
|
||||
endif
|
||||
|
||||
clean :
|
||||
@$(RM) -r $(ELF) $(OBJDUMP_NAME) $(LINKER_MAP_NAME) $(OBJ_ROOT_DIR) $(RUNTIME_ROOT_DIR) $(FORCE_REBUILD_DEP_LIST) ossc/menu_sjis.c
|
||||
@$(RM) -r $(ELF) $(OBJDUMP_NAME) $(LINKER_MAP_NAME) $(OBJ_ROOT_DIR) $(RUNTIME_ROOT_DIR) $(FORCE_REBUILD_DEP_LIST) ossc/menu_sjis.c ossc/userdata_sjis.c
|
||||
@$(ECHO) [$(APP_NAME) clean complete]
|
||||
|
||||
# Clean just the BSP.
|
||||
@ -1130,6 +1135,9 @@ print-elf-name:
|
||||
ossc/menu_sjis.c: ossc/menu.c
|
||||
iconv -f UTF-8 -t SHIFT-JIS ossc/menu.c > ossc/menu_sjis.c
|
||||
|
||||
ossc/userdata_sjis.c: ossc/userdata.c
|
||||
iconv -f UTF-8 -t SHIFT-JIS ossc/userdata.c > ossc/userdata_sjis.c
|
||||
|
||||
mem_init/sys_onchip_memory2_0.hex: sys_controller.elf
|
||||
$(RV_OBJCOPY) --change-addresses -0x10000 -O binary --gap-fill 0 $< mem_init/sys_onchip_memory2_0.bin
|
||||
../../tools/bin2hex 4 mem_init/sys_onchip_memory2_0.bin mem_init/sys_onchip_memory2_0.hex
|
||||
|
File diff suppressed because it is too large
Load Diff
138
software/sys_controller/ossc/fat16_export.c
Normal file
138
software/sys_controller/ossc/fat16_export.c
Normal file
@ -0,0 +1,138 @@
|
||||
//
|
||||
// Copyright (C) 2020 Ari Sundholm <megari@iki.fi>
|
||||
//
|
||||
// This file has been contributed to the Open Source Scan Converter project
|
||||
// developed by Markus Hiienkari Markus Hiienkari <mhiienka@niksula.hut.fi>
|
||||
// and other members of the retro gaming community.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
#include <string.h>
|
||||
#include "fat16_export.h"
|
||||
|
||||
/*
|
||||
* The beginning of the boot sector, along with the BPB.
|
||||
* Volume offsets 0x003 to 0x01a, inclusive.
|
||||
* The BPB spans volume offsets 0x00b to 0x01c, inclusive.
|
||||
*
|
||||
* The jump instruction at volume offsets 0x000 to 0x002, inclusive,
|
||||
* is left zeroed out to save a tiny bit of space.
|
||||
*/
|
||||
static const alt_u8 bootsec_beg_bpb_16[24] = {
|
||||
/* Three zeros */ 0x4d, 0x53, 0x57, 0x49, 0x4e, /* 0x003...0x007 */
|
||||
0x34, 0x2e, 0x31, 0x00, 0x02, 0x04, 0x80, 0x00, /* 0x008...0x00f */
|
||||
0x02, 0x00, 0x08, 0x00, 0x80, 0xf8, 0x20, 0x00, /* 0x010...0x017 */
|
||||
0x3f, 0x00, 0xff, /* Zeros until 0x024 */ /* 0x018...0x01a */
|
||||
};
|
||||
|
||||
/*
|
||||
* The rest of the boot sector before the boot code and terminator.
|
||||
* Offsets 0x024 to 0x03d, inclusive.
|
||||
*/
|
||||
static const alt_u8 bootsec_after_bpb_16[26] = {
|
||||
/* Zeros */ 0x80, 0x00, 0x29, 0xf4, /* 0x024...0x027 */
|
||||
0xcf, 0xc6, 0x04, 0x4f, 0x53, 0x53, 0x43, 0x50, /* 0x028...0x02f */
|
||||
0x52, 0x4f, 0x46, 0x49, 0x4c, 0x53, 0x46, 0x41, /* 0x030...0x037 */
|
||||
0x54, 0x31, 0x36, 0x20, 0x20, 0x20, /* Zeros */ /* 0x038...0x03d */
|
||||
};
|
||||
|
||||
/*
|
||||
* After this, we have the boot code (448 bytes) and sector terminator
|
||||
* (2 bytes). The former will be left zeroed-out and the latter will
|
||||
* be generated.
|
||||
*/
|
||||
|
||||
/* Generates a FAT16 boot sector.
|
||||
* buf must be at least FAT16_SECTOR_SIZE bytes long,
|
||||
* and is assumed to be pre-zeroed.
|
||||
*/
|
||||
void generate_boot_sector_16(alt_u8 *const buf) {
|
||||
/* Initial FAT16 boot sector contents + the BPB. */
|
||||
memcpy(buf + 3, bootsec_beg_bpb_16, 24);
|
||||
|
||||
/*
|
||||
* Then the rest of the boot sector.
|
||||
*
|
||||
* The boot code is supposed to be 448 bytes filled with 0xf4,
|
||||
* but leave it zeroed out to keep the code smaller. This may
|
||||
* be a deviation from the FAT16 spec, but should be harmless
|
||||
* for our purposes.
|
||||
*/
|
||||
memcpy(buf + 36, bootsec_after_bpb_16, 26);
|
||||
|
||||
/* RISC-V is little-endian, so do a 16-bit write instead. */
|
||||
*((alt_u16*)(buf + 510)) = 0xaa55U;
|
||||
}
|
||||
|
||||
/* The fixed 'preamble' of a FAT on a FAT16 volume. */
|
||||
static const alt_u32 fat16_preamble = 0xfffffff8U;
|
||||
|
||||
/*
|
||||
* Generate a FAT.
|
||||
* The buffer is assumed to be zeroed out and have a size of at least
|
||||
* FAT16_SECTOR_SIZE bytes.
|
||||
* The number of clusters already written is given as an argument.
|
||||
* The function returns the total number of clusters written so far.
|
||||
*
|
||||
* The intention is to be able to generate and write the FAT in chunks
|
||||
* that do not exhaust all the remaining RAM.
|
||||
*/
|
||||
alt_u16 generate_fat16(void *const buf, const alt_u16 written) {
|
||||
alt_u16 cur_ofs = 0;
|
||||
const alt_u16 start_cluster = 3U + written;
|
||||
alt_u16 *const fat = buf;
|
||||
|
||||
/*
|
||||
* The total number of FAT entries to write consists of:
|
||||
* 1. The FAT "preamble" (2 entries),
|
||||
* 2. The cluster chain of the file (512 entries).
|
||||
*
|
||||
* The latter needs to contain the chain terminator.
|
||||
*/
|
||||
const alt_u16 clusters_remaining = PROF_16_CLUSTER_COUNT - written;
|
||||
const alt_u16 preamble_compensation = written ? 0 : 2U;
|
||||
const alt_u16 clusters_to_write =
|
||||
((clusters_remaining > FAT16_ENTRIES_PER_SECTOR)
|
||||
? FAT16_ENTRIES_PER_SECTOR
|
||||
: clusters_remaining) - preamble_compensation;
|
||||
const alt_u16 end_cluster = start_cluster + clusters_to_write;
|
||||
const alt_u16 last_fat_cluster = PROF_16_CLUSTER_COUNT + 2U;
|
||||
|
||||
if (!written) {
|
||||
*((alt_u32*)fat) = fat16_preamble;
|
||||
cur_ofs += sizeof(fat16_preamble)/sizeof(alt_u16);
|
||||
}
|
||||
|
||||
for (alt_u16 cluster = start_cluster; cluster < end_cluster; ++cluster) {
|
||||
alt_u16 *const cur_entry = fat + cur_ofs;
|
||||
/* FAT16 entries are 16-bit little-endian. */
|
||||
if (cluster == last_fat_cluster) {
|
||||
/* At the last cluster, write the chain terminator. */
|
||||
*cur_entry = 0xffffU;
|
||||
}
|
||||
else {
|
||||
*cur_entry = cluster;
|
||||
}
|
||||
++cur_ofs;
|
||||
}
|
||||
|
||||
return end_cluster - 3U;
|
||||
}
|
||||
|
||||
const alt_u8 prof_dirent_16[PROF_DIRENT_16_SIZE] = {
|
||||
0x4f, 0x53, 0x53, 0x43, 0x50, 0x52, 0x4f, 0x46, 0x42, 0x49, 0x4e, 0x20,
|
||||
0x00, 0x8e, 0x04, 0xb5, 0x6f, 0x51, 0x6f, 0x51, 0x00, 0x00, 0x17, 0x89,
|
||||
0x6f, 0x51, 0x02, 0x00, 0x00, 0x00, 0x10, 0x00,
|
||||
};
|
83
software/sys_controller/ossc/fat16_export.h
Normal file
83
software/sys_controller/ossc/fat16_export.h
Normal file
@ -0,0 +1,83 @@
|
||||
//
|
||||
// Copyright (C) 2020 Ari Sundholm <megari@iki.fi>
|
||||
//
|
||||
// This file has been contributed to the Open Source Scan Converter project
|
||||
// developed by Markus Hiienkari Markus Hiienkari <mhiienka@niksula.hut.fi>
|
||||
// and other members of the retro gaming community.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
#ifndef FAT16_EXPORT_H_
|
||||
#define FAT16_EXPORT_H_
|
||||
|
||||
#include "alt_types.h"
|
||||
|
||||
/* Use a sector size of 512 bytes. */
|
||||
#define FAT16_SECTOR_SIZE 512U
|
||||
|
||||
/* This volume has 2048-byte clusters. */
|
||||
#define FAT16_CLUSTER_SIZE 2048U
|
||||
#define FAT16_SECTORS_PER_CLUSTER (FAT16_CLUSTER_SIZE/FAT16_SECTOR_SIZE)
|
||||
|
||||
/* Offsets of the two File Allocation Tables. */
|
||||
#define FAT16_1_OFS 0x10000UL
|
||||
#define FAT16_2_OFS 0x14000UL
|
||||
|
||||
/* Each FAT16 entry is a 16-bit little-endian integer. */
|
||||
#define FAT16_ENTRY_SIZE 2U
|
||||
#define FAT16_ENTRY_SHIFT 1U
|
||||
#define FAT16_ENTRIES_PER_SECTOR (FAT16_SECTOR_SIZE >> FAT16_ENTRY_SHIFT)
|
||||
|
||||
/* On this volume, each FAT will be 16 kiB in size. */
|
||||
#define FAT16_SIZE 0x04000UL
|
||||
|
||||
/* The first sector of the root directory. */
|
||||
#define FAT16_ROOT_DIR_FIRST_SECTOR 192U
|
||||
|
||||
/* The length of the root directory in sectors. */
|
||||
#define FAT16_ROOT_DIR_SECTORS 128U
|
||||
|
||||
/*
|
||||
* Define the properties and contents of the directory entry for the
|
||||
* settings file.
|
||||
*/
|
||||
#define PROF_DIRENT_16_OFS 0x18000UL
|
||||
#define PROF_DIRENT_16_SIZE 32U
|
||||
|
||||
extern const alt_u8 prof_dirent_16[PROF_DIRENT_16_SIZE];
|
||||
|
||||
#define PROF_16_DATA_OFS 0x028000UL
|
||||
#define PROF_16_DATA_SIZE 0x100000UL
|
||||
#define PROF_16_CLUSTER_COUNT (PROF_16_DATA_SIZE/FAT16_CLUSTER_SIZE)
|
||||
/* Profile file data starts at offset 0x00028000 */
|
||||
/* Profile file data ends at offset 0x00128000 */
|
||||
/* Profile file data is exactly 1 MiB long. */
|
||||
|
||||
|
||||
/* Generate a FAT16 boot sector.
|
||||
* buf must be at least FAT16_BOOT_SECTOR_SIZE bytes long,
|
||||
* and is assumed to be pre-zeroed.
|
||||
*/
|
||||
void generate_boot_sector_16(alt_u8 *buf);
|
||||
|
||||
/*
|
||||
* Generate a FAT of a FAT16 volume.
|
||||
* The buffer is assumed to be zeroed out and have a size of at least 512 bytes.
|
||||
* The number of clusters already written are given as an argument.
|
||||
* The function returns the total number of clusters written so far.
|
||||
*/
|
||||
alt_u16 generate_fat16(void *buf, alt_u16 written);
|
||||
|
||||
#endif // FAT16_EXPORT_H_
|
@ -29,12 +29,6 @@
|
||||
#define OPT_NOWRAP 0
|
||||
#define OPT_WRAP 1
|
||||
|
||||
#ifdef OSDLANG_JP
|
||||
#define LNG(e, j) j
|
||||
#else
|
||||
#define LNG(e, j) e
|
||||
#endif
|
||||
|
||||
extern char row1[LCD_ROW_LEN+1], row2[LCD_ROW_LEN+1], menu_row1[LCD_ROW_LEN+1], menu_row2[LCD_ROW_LEN+1];
|
||||
extern avmode_t cm;
|
||||
extern avconfig_t tc;
|
||||
@ -226,7 +220,7 @@ MENU(menu_audio, P99_PROTECT({ \
|
||||
MENU(menu_settings, P99_PROTECT({ \
|
||||
{ LNG("<Load profile >","<プロファイルロード >"), OPT_FUNC_CALL, { .fun = { load_profile, &profile_arg_info } } },
|
||||
{ LNG("<Save profile >","<プロファイルセーブ >"), OPT_FUNC_CALL, { .fun = { save_profile, &profile_arg_info } } },
|
||||
{ LNG("<Reset settings>","<セッテイオショキカ >"), OPT_FUNC_CALL, { .fun = { set_default_avconfig, NULL } } },
|
||||
{ LNG("<Reset settings>","<セッテイヲショキカ >"), OPT_FUNC_CALL, { .fun = { set_default_avconfig, NULL } } },
|
||||
{ LNG("Link prof->input","Link prof->input"), OPT_AVCONFIG_NUMVALUE, { .num = { &tc.link_av, OPT_WRAP, AV1_RGBs, AV_LAST, link_av_desc } } },
|
||||
{ LNG("Link input->prof","Link input->prof"), OPT_AVCONFIG_SELECTION, { .sel = { &profile_link, OPT_WRAP, SETTING_ITEM(off_on_desc) } } },
|
||||
{ LNG("Initial input","ショキニュウリョク"), OPT_AVCONFIG_SELECTION, { .sel = { &def_input, OPT_WRAP, SETTING_ITEM(avinput_str) } } },
|
||||
@ -252,9 +246,9 @@ MENU(menu_main, P99_PROTECT({ \
|
||||
{ LNG("Output opt. >","シュツリョクオプション >"), OPT_SUBMENU, { .sub = { &menu_output, NULL, NULL } } },
|
||||
{ LNG("Scanline opt. >","スキャンラインオプション >"), OPT_SUBMENU, { .sub = { &menu_scanlines, NULL, NULL } } },
|
||||
{ LNG("Post-proc. >","アトショリ >"), OPT_SUBMENU, { .sub = { &menu_postproc, NULL, NULL } } },
|
||||
{ LNG("Compatibility >","ゴカンセイ >"), OPT_SUBMENU, { .sub = { &menu_compatibility, NULL, NULL } } },
|
||||
{ LNG("Compatibility >","ゴカンセイ >"), OPT_SUBMENU, { .sub = { &menu_compatibility, NULL, NULL } } },
|
||||
AUDIO_MENU
|
||||
{ "Settings opt >", OPT_SUBMENU, { .sub = { &menu_settings, NULL, NULL } } },
|
||||
{ LNG("Settings opt >","セッテイカンリ >"), OPT_SUBMENU, { .sub = { &menu_settings, NULL, NULL } } },
|
||||
}))
|
||||
|
||||
// Max 3 levels currently
|
||||
|
@ -23,6 +23,12 @@
|
||||
#include "alt_types.h"
|
||||
#include "controls.h"
|
||||
|
||||
#ifdef OSDLANG_JP
|
||||
#define LNG(e, j) j
|
||||
#else
|
||||
#define LNG(e, j) e
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
OPT_AVCONFIG_SELECTION,
|
||||
OPT_AVCONFIG_NUMVALUE,
|
||||
|
@ -21,6 +21,7 @@
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include "userdata.h"
|
||||
#include "fat16_export.h"
|
||||
#include "flash.h"
|
||||
#include "sdcard.h"
|
||||
#include "firmware.h"
|
||||
@ -370,14 +371,22 @@ int export_userdata()
|
||||
alt_u8 prompt_state = 0;
|
||||
useconds_t prompt_delay;
|
||||
const alt_u8 prompt_transitions[] = { 1, 2, 0, 0, };
|
||||
const alt_u8 prompt_ofs[] = { 0, 16, 31, 48, };
|
||||
const alt_u8 prompt_ofs[] = { 0, 16, 31, LNG(48, 47), };
|
||||
const char *prompt_msgs =
|
||||
LNG(
|
||||
"SD CARD WILL BE" "\0" // [ 0..15]
|
||||
"OVERWRITTEN!!!" "\0" // [16..30]
|
||||
"Export? 1=Y, 2=N""\0" // [31..47]
|
||||
"Press 1 or 2"; // [48..60]
|
||||
"Press 1 or 2", // [48..60]
|
||||
"SDカードヲウワガキシマス" "\0" // [ 0..15]
|
||||
"ゴチュウイクダサイ!!!" "\0" // [16..30]
|
||||
"1=ジッコウスル 2=ヤメル" "\0" // [31..46]
|
||||
"ドチラカエランデクダサイ" // [47..60]
|
||||
);
|
||||
alt_u32 btn_vec;
|
||||
|
||||
_Static_assert(SD_BLK_SIZE == FAT16_SECTOR_SIZE, "Sector size mismatch");
|
||||
|
||||
retval = check_sdcard(databuf);
|
||||
SPI_CS_High();
|
||||
if (retval != 0) {
|
||||
@ -410,30 +419,76 @@ eval_button:
|
||||
prompt_state = 3;
|
||||
}
|
||||
|
||||
strncpy(menu_row2, "Exporting...", LCD_ROW_LEN+1);
|
||||
strncpy(menu_row2, LNG("Exporting...", "オマチクダサイ"), LCD_ROW_LEN+1);
|
||||
ui_disp_menu(2);
|
||||
|
||||
/* Zero out the boot sector, FATs and root directory. */
|
||||
memset(databuf, 0, SD_BLK_SIZE);
|
||||
for (alt_u32 sector = 0;
|
||||
sector < (FAT16_ROOT_DIR_FIRST_SECTOR + FAT16_ROOT_DIR_SECTORS);
|
||||
++sector)
|
||||
{
|
||||
retval = SD_Write(&sdcard_dev, databuf, sector);
|
||||
if (retval)
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* Generate and write the boot sector. */
|
||||
generate_boot_sector_16(databuf);
|
||||
retval = SD_Write(&sdcard_dev, databuf, 0);
|
||||
if (retval)
|
||||
goto out;
|
||||
|
||||
/* Generate and write the file allocation tables. */
|
||||
for (alt_u16 clusters_written = 0, sd_blk_idx = 0;
|
||||
clusters_written < (PROF_16_DATA_SIZE/FAT16_CLUSTER_SIZE);)
|
||||
{
|
||||
memset(databuf, 0, SD_BLK_SIZE);
|
||||
clusters_written = generate_fat16(databuf, clusters_written);
|
||||
retval = SD_Write(&sdcard_dev, databuf,
|
||||
(FAT16_1_OFS/SD_BLK_SIZE) + sd_blk_idx);
|
||||
if (retval)
|
||||
goto out;
|
||||
|
||||
retval = SD_Write(&sdcard_dev, databuf,
|
||||
(FAT16_2_OFS/SD_BLK_SIZE) + sd_blk_idx);
|
||||
if (retval)
|
||||
goto out;
|
||||
|
||||
++sd_blk_idx;
|
||||
}
|
||||
|
||||
/* Write the directory entry of the settings file. */
|
||||
memset(databuf, 0, SD_BLK_SIZE);
|
||||
memcpy(databuf, prof_dirent_16, PROF_DIRENT_16_SIZE);
|
||||
retval = SD_Write(&sdcard_dev, databuf, PROF_DIRENT_16_OFS/SD_BLK_SIZE);
|
||||
if (retval)
|
||||
goto out;
|
||||
|
||||
/* This may wear the SD card a bit more than necessary... */
|
||||
retval = copy_flash_to_sd(USERDATA_OFFSET/PAGESIZE, 512/SD_BLK_SIZE, (MAX_USERDATA_ENTRY + 1) * SECTORSIZE, databuf);
|
||||
retval = copy_flash_to_sd(USERDATA_OFFSET/PAGESIZE,
|
||||
PROF_16_DATA_OFS/SD_BLK_SIZE,
|
||||
(MAX_USERDATA_ENTRY + 1) * SECTORSIZE,
|
||||
databuf);
|
||||
|
||||
out:
|
||||
SPI_CS_High();
|
||||
|
||||
switch (retval) {
|
||||
case 0:
|
||||
msg = "Success";
|
||||
msg = LNG("Success", "カンリョウシマシタ"); // Alternative: "カンリョウイタシマシタ"
|
||||
break;
|
||||
case SD_NOINIT:
|
||||
msg = "No SD card det.";
|
||||
msg = LNG("No SD card det.", "SDカードガミツカリマセン");
|
||||
break;
|
||||
case -EINVAL:
|
||||
msg = "Invalid params.";
|
||||
msg = LNG("Invalid params.", "パラメータガムコウデス");
|
||||
break;
|
||||
case UDATA_EXPT_CANCELLED:
|
||||
msg = "Cancelled";
|
||||
msg = LNG("Cancelled", "キャンセルサレマシタ"); // Alternative: "キャンセルサセテイタダキマス"
|
||||
break;
|
||||
default:
|
||||
msg = "SD/Flash error";
|
||||
msg = LNG("SD/Flash error", "SDカFLASHノエラー"); // フラッシュ would be NG.
|
||||
break;
|
||||
}
|
||||
strncpy(menu_row2, msg, LCD_ROW_LEN+1);
|
||||
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user