mirror of
https://github.com/vgmstream/vgmstream.git
synced 2025-01-17 23:36:41 +01:00
Add EA .SNS+SNR / .SPS [Burnout Crash, NFS Hot Pursuit PS3]
This commit is contained in:
parent
77b849a024
commit
e2c059bc31
@ -278,6 +278,7 @@ VGMSTREAM_DECLARE_FILE_TYPE("SMPL", smpl);
|
||||
VGMSTREAM_DECLARE_FILE_TYPE("SND", snd);
|
||||
VGMSTREAM_DECLARE_FILE_TYPE("SNDS", snds);
|
||||
VGMSTREAM_DECLARE_FILE_TYPE("SNG", sng);
|
||||
VGMSTREAM_DECLARE_FILE_TYPE("SNR", snr);
|
||||
VGMSTREAM_DECLARE_FILE_TYPE("SNS", sns);
|
||||
VGMSTREAM_DECLARE_FILE_TYPE("SNU", snu);
|
||||
VGMSTREAM_DECLARE_FILE_TYPE("SPD", spd);
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
#ifdef VGM_USE_FFMPEG
|
||||
|
||||
#define EAXMA_XMA_MAX_PACKETS_PER_SNS_BLOCK 3 /* only seen up to 3 (Dante's Inferno) */
|
||||
#define EAXMA_XMA_MAX_PACKETS_PER_SNS_BLOCK 4 /* normally max 3 (Dante's Inferno), ~14 (1 stream) in Burnout Paradise */
|
||||
#define EAXMA_XMA_MAX_STREAMS_PER_SNS_BLOCK 4 /* XMA2 max is 8ch = 4 * 2ch */
|
||||
#define EAXMA_XMA_PACKET_SIZE 0x800
|
||||
#define EAXMA_XMA_BUFFER_SIZE (EAXMA_XMA_MAX_PACKETS_PER_SNS_BLOCK * EAXMA_XMA_MAX_STREAMS_PER_SNS_BLOCK * EAXMA_XMA_PACKET_SIZE)
|
||||
@ -48,7 +48,8 @@ int ffmpeg_custom_read_eaxma(ffmpeg_codec_data *data, uint8_t *buf, int buf_size
|
||||
if (max_packets == 0) goto fail;
|
||||
|
||||
if (max_packets * num_streams * EAXMA_XMA_PACKET_SIZE > EAXMA_XMA_BUFFER_SIZE) {
|
||||
VGM_LOG("EA XMA: block too big at %lx\n", (off_t)real_offset);
|
||||
VGM_LOG("EA XMA: block too big (%i * %i * 0x%x = 0x%x vs max 0x%x) at %lx\n",
|
||||
max_packets,num_streams,EAXMA_XMA_PACKET_SIZE, max_packets*num_streams*EAXMA_XMA_PACKET_SIZE, EAXMA_XMA_BUFFER_SIZE,(off_t)real_offset);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
|
@ -273,6 +273,7 @@ static const char* extension_list[] = {
|
||||
"snd",
|
||||
"snds",
|
||||
"sng",
|
||||
"snr",
|
||||
"sns",
|
||||
"snu",
|
||||
"spd",
|
||||
@ -911,6 +912,8 @@ static const meta_info meta_info_list[] = {
|
||||
{meta_NAAC, "Namco NAAC header"},
|
||||
{meta_EZW, "EZ2DJ EZWAVE header"},
|
||||
{meta_VXN, "Gameloft VXN header"},
|
||||
{meta_EA_SNR_SNS, "Electronic Arts SNR+SNS header"},
|
||||
{meta_EA_SPS, "Electronic Arts SPS header"},
|
||||
|
||||
#ifdef VGM_USE_VORBIS
|
||||
{meta_OGG_VORBIS, "Ogg Vorbis"},
|
||||
|
@ -21,9 +21,14 @@ void block_update_ea_sns(off_t block_offset, VGMSTREAM * vgmstream) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* known: 0x80 = last block, 0x40, 0x08, 0x04, 0x01 */
|
||||
/* 0x80: last block
|
||||
* 0x40: new block for some codecs?
|
||||
* 0x08: ?
|
||||
* 0x04: new block for some codecs?
|
||||
* 0x01: last block for some codecs?
|
||||
* 0x00: none? */
|
||||
if (block_size & 0xFF000000) {
|
||||
VGM_ASSERT(!(block_size & 0x80000000), "EA SNS: unknown flag found at %lx\n", block_offset);
|
||||
//VGM_ASSERT(!(block_size & 0x80000000), "EA SNS: unknown flag found at %lx\n", block_offset);
|
||||
block_size &= 0x00FFFFFF;
|
||||
}
|
||||
|
||||
|
@ -2,16 +2,69 @@
|
||||
#include "../layout/layout.h"
|
||||
#include "../coding/coding.h"
|
||||
|
||||
static VGMSTREAM * init_vgmstream_eaaudiocore_header(STREAMFILE * streamHead, STREAMFILE * streamData, off_t header_offset, off_t start_offset, meta_t meta_type);
|
||||
|
||||
/* .SNU - from EA Redwood Shores/Visceral games (Dead Space, Dante's Inferno, The Godfather 2) */
|
||||
/* .SNR+SNS - from EA latest games (~2008-2013), v0 header */
|
||||
VGMSTREAM * init_vgmstream_ea_snr_sns(STREAMFILE * streamFile) {
|
||||
VGMSTREAM * vgmstream = NULL;
|
||||
STREAMFILE * streamData = NULL;
|
||||
|
||||
/* check extension, case insensitive */
|
||||
if (!check_extensions(streamFile,"snr"))
|
||||
goto fail;
|
||||
|
||||
/* SNR headers normally need an external SNS file, but some have data */
|
||||
if (get_streamfile_size(streamFile) > 0x0c) {
|
||||
vgmstream = init_vgmstream_eaaudiocore_header(streamFile, streamFile, 0x00, 0x0c, meta_EA_SNR_SNS);
|
||||
if (!vgmstream) goto fail;
|
||||
}
|
||||
else {
|
||||
streamData = open_stream_ext(streamFile,"sns");
|
||||
if (!streamData) goto fail;
|
||||
|
||||
vgmstream = init_vgmstream_eaaudiocore_header(streamFile, streamData, 0x00, 0x00, meta_EA_SNR_SNS);
|
||||
if (!vgmstream) goto fail;
|
||||
}
|
||||
|
||||
if (streamData) close_streamfile(streamData);
|
||||
return vgmstream;
|
||||
|
||||
fail:
|
||||
if (streamData) close_streamfile(streamData);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* .SPS - from EA latest games (~2014), v1 header */
|
||||
VGMSTREAM * init_vgmstream_ea_sps(STREAMFILE * streamFile) {
|
||||
VGMSTREAM * vgmstream = NULL;
|
||||
off_t start_offset;
|
||||
|
||||
/* check extension, case insensitive */
|
||||
if (!check_extensions(streamFile,"sps"))
|
||||
goto fail;
|
||||
|
||||
/* seems to be fixed */
|
||||
if ((read_32bitBE(0x00,streamFile) & 0xFFFFFF00) != 0x48000000)
|
||||
goto fail;
|
||||
|
||||
start_offset = read_8bit(0x03, streamFile);
|
||||
|
||||
vgmstream = init_vgmstream_eaaudiocore_header(streamFile, streamFile, 0x04, start_offset, meta_EA_SPS);
|
||||
if (!vgmstream) goto fail;
|
||||
|
||||
return vgmstream;
|
||||
|
||||
fail:
|
||||
close_vgmstream(vgmstream);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* .SNU - from EA Redwood Shores/Visceral games (Dead Space, Dante's Inferno, The Godfather 2), v0 header */
|
||||
VGMSTREAM * init_vgmstream_ea_snu(STREAMFILE *streamFile) {
|
||||
VGMSTREAM * vgmstream = NULL;
|
||||
int channel_count, loop_flag = 0, version, codec, channel_config, sample_rate, flags;
|
||||
uint32_t num_samples, loop_start = 0, loop_end = 0;
|
||||
off_t start_offset, header_offset;
|
||||
int32_t (*read_32bit)(off_t,STREAMFILE*) = NULL;
|
||||
|
||||
|
||||
/* check extension, case insensitive */
|
||||
if (!check_extensions(streamFile,"snu"))
|
||||
goto fail;
|
||||
@ -32,26 +85,37 @@ VGMSTREAM * init_vgmstream_ea_snu(STREAMFILE *streamFile) {
|
||||
read_32bit = read_32bitLE;
|
||||
}
|
||||
|
||||
header_offset = 0x10; /* points to EA SNH/SPH header */
|
||||
start_offset = read_32bit(0x08,streamFile); /* points to EA SNS/SPS blocks */
|
||||
header_offset = 0x10; /* SNR header */
|
||||
start_offset = read_32bit(0x08,streamFile); /* SPS blocks */
|
||||
|
||||
vgmstream = init_vgmstream_eaaudiocore_header(streamFile, streamFile, header_offset, start_offset, meta_EA_SNU);
|
||||
if (!vgmstream) goto fail;
|
||||
|
||||
return vgmstream;
|
||||
|
||||
fail:
|
||||
close_vgmstream(vgmstream);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
/* Beyond is the newest EA header (from EAAudioCore library) still generated by sx.exe.
|
||||
* Its audio "assets" come in separate RAM headers (.SNR/SPH) and raw blocked streams (.SNS/SPS),
|
||||
* or together in pseudoformats (.SNU, .SBR+.SBS banks, .AEMS, .MUS, etc).
|
||||
* Some .SNR include stream data, while most (all?) .SPS have headers so .SPH is optional. */
|
||||
/* EA newest header from RwAudioCore/EAAudioCore library (still generated by sx.exe).
|
||||
* Audio "assets" come in separate RAM headers (.SNR/SPH) and raw blocked streams (.SNS/SPS),
|
||||
* or together in pseudoformats (.SNU, .SBR+.SBS banks, .AEMS, .MUS, etc).
|
||||
* Some .SNR include stream data, while .SPS have headers so .SPH is optional. */
|
||||
static VGMSTREAM * init_vgmstream_eaaudiocore_header(STREAMFILE * streamHead, STREAMFILE * streamData, off_t header_offset, off_t start_offset, meta_t meta_type) {
|
||||
VGMSTREAM * vgmstream = NULL;
|
||||
int channel_count, loop_flag = 0, version, codec, channel_config, sample_rate, flags;
|
||||
uint32_t num_samples, loop_start = 0, loop_end = 0;
|
||||
|
||||
/* EA SNR header */
|
||||
version = (read_8bit(header_offset + 0x00,streamFile) >> 4) & 0xf;
|
||||
codec = (read_8bit(header_offset + 0x00,streamFile) >> 0) & 0xf;
|
||||
channel_config = read_8bit(header_offset + 0x01,streamFile);
|
||||
sample_rate = (uint16_t)read_16bitBE(header_offset + 0x02,streamFile);
|
||||
flags = (uint8_t)read_8bit(header_offset + 0x04,streamFile); /* upper nibble only? */
|
||||
num_samples = (uint32_t)read_32bitBE(header_offset + 0x04,streamFile) & 0x00FFFFFF;
|
||||
/* optional, in some headers:
|
||||
* 0x08: null?
|
||||
* 0x0c: varies (ex. null, full size) */
|
||||
/* headered .SPS start with an id of 0x480000xx (not present in .SPH = not part of the header) */
|
||||
/* EA SNR/SPH header */
|
||||
version = (read_8bit(header_offset + 0x00,streamHead) >> 4) & 0xf;
|
||||
codec = (read_8bit(header_offset + 0x00,streamHead) >> 0) & 0xf;
|
||||
channel_config = read_8bit(header_offset + 0x01,streamHead);
|
||||
sample_rate = (uint16_t)read_16bitBE(header_offset + 0x02,streamHead);
|
||||
flags = (uint8_t)read_8bit(header_offset + 0x04,streamHead); /* upper nibble only? */
|
||||
num_samples = (uint32_t)read_32bitBE(header_offset + 0x04,streamHead) & 0x00FFFFFF;
|
||||
/* optional, in some headers: 0x08: null? 0x0c: varies (ex. null, full size) */
|
||||
|
||||
/* V0: SNR+SNS, V1: SPR+SPS (not apparent differences) */
|
||||
if (version != 0 && version != 1) {
|
||||
@ -59,13 +123,13 @@ VGMSTREAM * init_vgmstream_ea_snu(STREAMFILE *streamFile) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
/* & 0x40: stream asset?, 0x20: full loop?, 0x00: RAM asset?, 0x01: loop? */
|
||||
if (flags != 0x60 && flags != 0x40) {
|
||||
/* 0x40: stream asset, 0x20: full loop, 0x00: default/RAM asset, 0x01: loop? */
|
||||
if (flags != 0x60 && flags != 0x40 && flags != 0x20 && flags != 0x00) {
|
||||
VGM_LOG("EA SNS/SPS: unknown flag 0x%02x\n", flags);
|
||||
}
|
||||
|
||||
/* full loop seen in Dead Space ambient tracks */
|
||||
if (flags == 0x60) {
|
||||
/* seen in sfx and Dead Space ambient tracks */
|
||||
if (flags & 0x20) {
|
||||
VGM_LOG("flg=%x\n",flags);
|
||||
loop_flag = 1;
|
||||
loop_start = 0;
|
||||
@ -73,7 +137,7 @@ VGMSTREAM * init_vgmstream_ea_snu(STREAMFILE *streamFile) {
|
||||
}
|
||||
|
||||
/* accepted channel configs only seem to be mono/stereo/quad/5.1/7.1 */
|
||||
//channel_count = ((channel_config >> 2) & 0xf) + 1;
|
||||
//channel_count = ((channel_config >> 2) & 0xf) + 1; /* likely, but better fail with unknown values */
|
||||
switch(channel_config) {
|
||||
case 0x00: channel_count = 1; break;
|
||||
case 0x04: channel_count = 2; break;
|
||||
@ -94,55 +158,37 @@ VGMSTREAM * init_vgmstream_ea_snu(STREAMFILE *streamFile) {
|
||||
vgmstream->num_samples = num_samples;
|
||||
vgmstream->loop_start_sample = loop_start;
|
||||
vgmstream->loop_end_sample = loop_end;
|
||||
|
||||
vgmstream->meta_type = meta_EA_SNU;
|
||||
vgmstream->meta_type = meta_type;
|
||||
|
||||
/* EA decoder list and known internal FourCCs */
|
||||
switch(codec) {
|
||||
case 0x04: /* "Xas1": EA-XAS (Dead Space PC/PS3) */
|
||||
vgmstream->coding_type = coding_EA_XAS;
|
||||
|
||||
case 0x02: /* "P6B0": PCM16BE */
|
||||
vgmstream->coding_type = coding_PCM16_int;
|
||||
vgmstream->codec_endian = 1;
|
||||
vgmstream->layout_type = layout_blocked_ea_sns;
|
||||
break;
|
||||
|
||||
#if 0
|
||||
#ifdef VGM_USE_MPEG
|
||||
case 0x07: { /* "L32S": EALayer3 v2 "S" (Dante's Inferno PS3) */
|
||||
mpeg_custom_config cfg;
|
||||
off_t mpeg_start_offset = start_offset + 0x08;
|
||||
|
||||
memset(&cfg, 0, sizeof(mpeg_custom_config));
|
||||
|
||||
/* layout is still blocks, but should work fine with the custom mpeg decoder */
|
||||
vgmstream->codec_data = init_mpeg_custom_codec_data(streamFile, mpeg_start_offset, &vgmstream->coding_type, vgmstream->channels, MPEG_EAL32S, &cfg);
|
||||
if (!vgmstream->codec_data) goto fail;
|
||||
|
||||
vgmstream->layout_type = layout_blocked_ea_sns;
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef VGM_USE_FFMPEG
|
||||
case 0x03: { /* "EXm0": EA-XMA (Dante's Inferno X360) */
|
||||
uint8_t buf[0x100];
|
||||
int bytes, block_size, block_count;
|
||||
size_t stream_size, virtual_size;
|
||||
ffmpeg_custom_config cfg;
|
||||
ffmpeg_custom_config cfg = {0};
|
||||
|
||||
stream_size = get_streamfile_size(streamFile) - start_offset;
|
||||
virtual_size = ffmpeg_get_eaxma_virtual_size(vgmstream->channels, start_offset,stream_size, streamFile);
|
||||
stream_size = get_streamfile_size(streamData) - start_offset;
|
||||
virtual_size = ffmpeg_get_eaxma_virtual_size(vgmstream->channels, start_offset,stream_size, streamData);
|
||||
block_size = 0x10000; /* todo unused and not correctly done by the parser */
|
||||
block_count = stream_size / block_size + (stream_size % block_size ? 1 : 0);
|
||||
|
||||
bytes = ffmpeg_make_riff_xma2(buf, 0x100, vgmstream->num_samples, virtual_size, vgmstream->channels, vgmstream->sample_rate, block_count, block_size);
|
||||
if (bytes <= 0) goto fail;
|
||||
|
||||
memset(&cfg, 0, sizeof(ffmpeg_custom_config));
|
||||
cfg.type = FFMPEG_EA_XMA;
|
||||
cfg.virtual_size = virtual_size;
|
||||
cfg.channels = vgmstream->channels;
|
||||
|
||||
vgmstream->codec_data = init_ffmpeg_config(streamFile, buf,bytes, start_offset,stream_size, &cfg);
|
||||
vgmstream->codec_data = init_ffmpeg_config(streamData, buf,bytes, start_offset,stream_size, &cfg);
|
||||
if (!vgmstream->codec_data) goto fail;
|
||||
|
||||
vgmstream->coding_type = coding_FFmpeg;
|
||||
@ -151,17 +197,34 @@ VGMSTREAM * init_vgmstream_ea_snu(STREAMFILE *streamFile) {
|
||||
}
|
||||
#endif
|
||||
|
||||
case 0x00: /* "NONE" (internal codec not set flag) */
|
||||
case 0x01: /* not used/reserved? MP30/P6L0/P2B0/P2L0/P8S0/P8U0/PFN0? */
|
||||
case 0x02: /* "P6B0": PCM16BE */
|
||||
case 0x05: /* "EL31": EALayer3 v1 b (with PCM blocks in normal EA-frames?) */
|
||||
case 0x06: /* "L32P": EALayer3 v2 "P" */
|
||||
case 0x04: /* "Xas1": EA-XAS (Dead Space PC/PS3) */
|
||||
vgmstream->coding_type = coding_EA_XAS;
|
||||
vgmstream->layout_type = layout_blocked_ea_sns;
|
||||
break;
|
||||
|
||||
#ifdef VGM_USE_MPEG
|
||||
case 0x05: /* "EL31": EALayer3 v1 (Need for Speed: Hot Pursuit PS3) */
|
||||
case 0x06: /* "L32P": EALayer3 v2 "PCM" (Battlefield 1943 PS3) */
|
||||
case 0x07: { /* "L32S": EALayer3 v2 "Spike" (Dante's Inferno PS3) */
|
||||
mpeg_custom_config cfg = {0};
|
||||
off_t mpeg_start_offset = start_offset + 0x08;
|
||||
mpeg_custom_t type = (codec == 0x05 ? MPEG_EAL31b : (codec == 0x06) ? MPEG_EAL32P : MPEG_EAL32S);
|
||||
|
||||
/* layout is still blocks, but should work fine with the custom mpeg decoder */
|
||||
vgmstream->codec_data = init_mpeg_custom_codec_data(streamData, mpeg_start_offset, &vgmstream->coding_type, vgmstream->channels, type, &cfg);
|
||||
if (!vgmstream->codec_data) goto fail;
|
||||
|
||||
vgmstream->layout_type = layout_blocked_ea_sns;
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
case 0x00: /* "NONE" (internal 'codec not set' flag) */
|
||||
case 0x01: /* not used/reserved? MP30/P6L0/P2B0/P2L0/P8S0/P8U0/PFN0? */
|
||||
case 0x08: /* ? */
|
||||
case 0x09: /* EASpeex? "Esp0" (libspeex variant) */
|
||||
case 0x0a: /* EATrax (ATRAC9 variant, deflated frames) */
|
||||
case 0x09: /* EASpeex (libspeex variant, base versions vary: 1.0.5, 1.2beta3) */
|
||||
case 0x0a: /* EATrax (ATRAC9 variant, has deflated fillers a la EA-XMA) */
|
||||
case 0x0b: /* ? */
|
||||
case 0x0c: /* EAOpus (inside each SNS/SPS block is 16b frame size + Opus standard? packet) */
|
||||
case 0x0c: /* EAOpus (inside each SNS/SPS block is 16b frame size + standard? Opus packet) */
|
||||
case 0x0d: /* ? */
|
||||
case 0x0e: /* ? */
|
||||
case 0x0f: /* ? */
|
||||
@ -172,7 +235,7 @@ VGMSTREAM * init_vgmstream_ea_snu(STREAMFILE *streamFile) {
|
||||
|
||||
|
||||
/* open the file for reading by each channel */
|
||||
if (!vgmstream_open_stream(vgmstream,streamFile,start_offset))
|
||||
if (!vgmstream_open_stream(vgmstream,streamData,start_offset))
|
||||
goto fail;
|
||||
|
||||
if (vgmstream->layout_type == layout_blocked_ea_sns)
|
||||
|
@ -695,4 +695,7 @@ VGMSTREAM * init_vgmstream_ezw(STREAMFILE * streamFile);
|
||||
|
||||
VGMSTREAM * init_vgmstream_vxn(STREAMFILE * streamFile);
|
||||
|
||||
VGMSTREAM * init_vgmstream_ea_snr_sns(STREAMFILE * streamFile);
|
||||
|
||||
VGMSTREAM * init_vgmstream_ea_sps(STREAMFILE * streamFile);
|
||||
#endif /*_META_H*/
|
||||
|
@ -367,6 +367,8 @@ VGMSTREAM * (*init_vgmstream_functions[])(STREAMFILE *streamFile) = {
|
||||
init_vgmstream_ubi_sb,
|
||||
init_vgmstream_ezw,
|
||||
init_vgmstream_vxn,
|
||||
init_vgmstream_ea_snr_sns,
|
||||
init_vgmstream_ea_sps,
|
||||
|
||||
init_vgmstream_txth, /* should go at the end (lower priority) */
|
||||
#ifdef VGM_USE_FFMPEG
|
||||
|
@ -630,6 +630,8 @@ typedef enum {
|
||||
meta_UBI_SB, /* Ubisoft banks */
|
||||
meta_EZW, /* EZ2DJ (Arcade) EZWAV */
|
||||
meta_VXN, /* Gameloft mobile games */
|
||||
meta_EA_SNR_SNS, /* Electronic Arts SNR+SNS (Burnout Paradise) */
|
||||
meta_EA_SPS, /* Electronic Arts SPS (Burnout Crash) */
|
||||
|
||||
#ifdef VGM_USE_VORBIS
|
||||
meta_OGG_VORBIS, /* Ogg Vorbis */
|
||||
@ -871,9 +873,10 @@ typedef enum {
|
||||
MPEG_FSB, /* N streams of 1 data-frame+padding (=interleave) */
|
||||
MPEG_P3D, /* N streams of fixed interleave (not frame-aligned) */
|
||||
MPEG_EA, /* 1 stream (maybe N streams in absolute offsets?) */
|
||||
MPEG_EAL31, /* EALayer3 v1, custom frames with v1 header */
|
||||
MPEG_EAL32P, /* EALayer3 v2 "P" (PCM?), custom frames with v2 header */
|
||||
MPEG_EAL32S, /* EALayer3 v2 "S" (Spike?), custom frames with v2 header */
|
||||
MPEG_EAL31, /* EALayer3 v1 (SCHl), custom frames with v1 header */
|
||||
MPEG_EAL31b, /* EALayer3 v1 (SNS), custom frames with v1 header + minor changes */
|
||||
MPEG_EAL32P, /* EALayer3 v2 "PCM", custom frames with v2 header + bigger PCM blocks? */
|
||||
MPEG_EAL32S, /* EALayer3 v2 "Spike", custom frames with v2 header + smaller PCM blocks? */
|
||||
MPEG_LYN, /* N streams of fixed interleave */
|
||||
MPEG_AWC /* N streams in block layout (music) or absolute offsets (sfx) */
|
||||
} mpeg_custom_t;
|
||||
|
Loading…
x
Reference in New Issue
Block a user