mirror of
https://github.com/ZDoom/gzdoom.git
synced 2024-11-25 13:31:37 +00:00
implement the different reader types.
This commit is contained in:
parent
11d6b3e5b4
commit
737e3f22d7
12 changed files with 73 additions and 43 deletions
|
@ -218,7 +218,7 @@ FileReader FZipPatReader::OpenFile(const char *name)
|
||||||
auto lump = resf->FindEntry(name);
|
auto lump = resf->FindEntry(name);
|
||||||
if (lump >= 0)
|
if (lump >= 0)
|
||||||
{
|
{
|
||||||
return resf->GetEntryReader(lump);
|
return resf->GetEntryReader(lump, FileSys::READER_NEW, FileSys::READERFLAG_SEEKABLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fr.OpenFile(name);
|
fr.OpenFile(name);
|
||||||
|
|
|
@ -31,7 +31,8 @@ enum EDecompressFlags
|
||||||
{
|
{
|
||||||
DCF_TRANSFEROWNER = 1,
|
DCF_TRANSFEROWNER = 1,
|
||||||
DCF_SEEKABLE = 2,
|
DCF_SEEKABLE = 2,
|
||||||
DCF_EXCEPTIONS = 4
|
DCF_EXCEPTIONS = 4,
|
||||||
|
DCF_CACHED = 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
bool OpenDecompressor(FileReader& self, FileReader &parent, FileReader::Size length, int method, int flags = 0); // creates a decompressor stream. 'seekable' uses a buffered version so that the Seek and Tell methods can be used.
|
bool OpenDecompressor(FileReader& self, FileReader &parent, FileReader::Size length, int method, int flags = 0); // creates a decompressor stream. 'seekable' uses a buffered version so that the Seek and Tell methods can be used.
|
||||||
|
|
|
@ -93,10 +93,19 @@ public:
|
||||||
FileData ReadFile (const char *name) { return ReadFile (GetNumForName (name)); }
|
FileData ReadFile (const char *name) { return ReadFile (GetNumForName (name)); }
|
||||||
FileData ReadFileFullName(const char* name) { return ReadFile(GetNumForFullName(name)); }
|
FileData ReadFileFullName(const char* name) { return ReadFile(GetNumForFullName(name)); }
|
||||||
|
|
||||||
FileReader OpenFileReader(int lump); // opens a reader that redirects to the containing file's one.
|
FileReader OpenFileReader(int lump, int readertype, int readerflags); // opens a reader that redirects to the containing file's one.
|
||||||
FileReader ReopenFileReader(int lump, bool alwayscache = false); // opens an independent reader.
|
|
||||||
FileReader OpenFileReader(const char* name);
|
FileReader OpenFileReader(const char* name);
|
||||||
FileReader ReopenFileReader(const char* name, bool alwayscache = false);
|
FileReader ReopenFileReader(const char* name, bool alwayscache = false);
|
||||||
|
FileReader OpenFileReader(int lump)
|
||||||
|
{
|
||||||
|
return OpenFileReader(lump, READER_SHARED, READERFLAG_SEEKABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileReader ReopenFileReader(int lump, bool alwayscache = false)
|
||||||
|
{
|
||||||
|
return OpenFileReader(lump, alwayscache ? READER_CACHED : READER_NEW, READERFLAG_SEEKABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int FindLump (const char *name, int *lastlump, bool anyns=false); // [RH] Find lumps with duplication
|
int FindLump (const char *name, int *lastlump, bool anyns=false); // [RH] Find lumps with duplication
|
||||||
int FindLumpMulti (const char **names, int *lastlump, bool anyns = false, int *nameindex = NULL); // same with multiple possible names
|
int FindLumpMulti (const char **names, int *lastlump, bool anyns = false, int *nameindex = NULL); // same with multiple possible names
|
||||||
|
|
|
@ -86,6 +86,14 @@ enum ELumpFlags
|
||||||
RESFF_NEEDFILESTART = 32, // The real position is not known yet and needs to be calculated on access
|
RESFF_NEEDFILESTART = 32, // The real position is not known yet and needs to be calculated on access
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum EReaderType
|
||||||
|
{
|
||||||
|
READER_SHARED = 0, // returns a view into the parent's reader.
|
||||||
|
READER_NEW = 1, // opens a new file handle
|
||||||
|
READER_CACHED = 2, // returns a MemoryArrayReader
|
||||||
|
READERFLAG_SEEKABLE = 1 // ensure the reader is seekable.
|
||||||
|
};
|
||||||
|
|
||||||
struct FResourceEntry
|
struct FResourceEntry
|
||||||
{
|
{
|
||||||
size_t Length;
|
size_t Length;
|
||||||
|
@ -156,7 +164,8 @@ public:
|
||||||
return (entry < NumLumps) ? Entries[entry].Position : 0;
|
return (entry < NumLumps) ? Entries[entry].Position : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual FileReader GetEntryReader(uint32_t entry, bool newreader = true);
|
// default is the safest reader type.
|
||||||
|
virtual FileReader GetEntryReader(uint32_t entry, int readertype = READER_NEW, int flags = READERFLAG_SEEKABLE);
|
||||||
|
|
||||||
int GetEntryFlags(uint32_t entry)
|
int GetEntryFlags(uint32_t entry)
|
||||||
{
|
{
|
||||||
|
@ -180,8 +189,8 @@ public:
|
||||||
|
|
||||||
virtual FileData Read(int entry)
|
virtual FileData Read(int entry)
|
||||||
{
|
{
|
||||||
auto fr = GetEntryReader(entry, false);
|
auto fr = GetEntryReader(entry, READER_SHARED, 0);
|
||||||
return fr.Read();
|
return fr.Read(entry < NumLumps ? Entries[entry].Length : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual FCompressedBuffer GetRawData(uint32_t entry);
|
virtual FCompressedBuffer GetRawData(uint32_t entry);
|
||||||
|
|
|
@ -180,7 +180,7 @@ public:
|
||||||
bool Open(LumpFilterInfo* filter, FileSystemMessageFunc Printf);
|
bool Open(LumpFilterInfo* filter, FileSystemMessageFunc Printf);
|
||||||
virtual ~F7ZFile();
|
virtual ~F7ZFile();
|
||||||
FileData Read(int entry) override;
|
FileData Read(int entry) override;
|
||||||
FileReader GetEntryReader(uint32_t entry, bool) override;
|
FileReader GetEntryReader(uint32_t entry, int, int) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -335,7 +335,7 @@ FileData F7ZFile::Read(int entry)
|
||||||
//
|
//
|
||||||
//==========================================================================
|
//==========================================================================
|
||||||
|
|
||||||
FileReader F7ZFile::GetEntryReader(uint32_t entry, bool)
|
FileReader F7ZFile::GetEntryReader(uint32_t entry, int, int)
|
||||||
{
|
{
|
||||||
FileReader fr;
|
FileReader fr;
|
||||||
if (entry < 0 || entry >= NumLumps) return fr;
|
if (entry < 0 || entry >= NumLumps) return fr;
|
||||||
|
|
|
@ -64,7 +64,7 @@ class FDirectory : public FResourceFile
|
||||||
public:
|
public:
|
||||||
FDirectory(const char * dirname, StringPool* sp, bool nosubdirflag = false);
|
FDirectory(const char * dirname, StringPool* sp, bool nosubdirflag = false);
|
||||||
bool Open(LumpFilterInfo* filter, FileSystemMessageFunc Printf);
|
bool Open(LumpFilterInfo* filter, FileSystemMessageFunc Printf);
|
||||||
FileReader GetEntryReader(uint32_t entry, bool newreader = true) override;
|
FileReader GetEntryReader(uint32_t entry, int, int) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -162,13 +162,18 @@ bool FDirectory::Open(LumpFilterInfo* filter, FileSystemMessageFunc Printf)
|
||||||
//
|
//
|
||||||
//==========================================================================
|
//==========================================================================
|
||||||
|
|
||||||
FileReader FDirectory::GetEntryReader(uint32_t entry, bool newreader)
|
FileReader FDirectory::GetEntryReader(uint32_t entry, int readertype, int)
|
||||||
{
|
{
|
||||||
FileReader fr;
|
FileReader fr;
|
||||||
if (entry < NumLumps)
|
if (entry < NumLumps)
|
||||||
{
|
{
|
||||||
std::string fn = mBasePath; fn += Entries[entry].FileName;
|
std::string fn = mBasePath; fn += Entries[entry].FileName;
|
||||||
fr.OpenFile(fn.c_str());
|
fr.OpenFile(fn.c_str());
|
||||||
|
if (readertype == READER_CACHED)
|
||||||
|
{
|
||||||
|
auto data = fr.Read();
|
||||||
|
fr.OpenMemoryArray(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return fr;
|
return fr;
|
||||||
}
|
}
|
||||||
|
|
|
@ -940,7 +940,7 @@ bool OpenDecompressor(FileReader& self, FileReader &parent, FileReader::Size len
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
fr = new MemoryArrayReader(buffer);
|
fr = new MemoryArrayReader(buffer);
|
||||||
flags &= ~DCF_SEEKABLE;
|
flags &= ~(DCF_SEEKABLE | DCF_CACHED);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -949,7 +949,7 @@ bool OpenDecompressor(FileReader& self, FileReader &parent, FileReader::Size len
|
||||||
FileData buffer(nullptr, length);
|
FileData buffer(nullptr, length);
|
||||||
ShrinkLoop(buffer.writable(), length, *p, p->GetLength()); // this never fails.
|
ShrinkLoop(buffer.writable(), length, *p, p->GetLength()); // this never fails.
|
||||||
fr = new MemoryArrayReader(buffer);
|
fr = new MemoryArrayReader(buffer);
|
||||||
flags &= ~DCF_SEEKABLE;
|
flags &= ~(DCF_SEEKABLE | DCF_CACHED);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -965,7 +965,7 @@ bool OpenDecompressor(FileReader& self, FileReader &parent, FileReader::Size len
|
||||||
bufr[i] ^= i >> 1;
|
bufr[i] ^= i >> 1;
|
||||||
}
|
}
|
||||||
fr = new MemoryArrayReader(buffer);
|
fr = new MemoryArrayReader(buffer);
|
||||||
flags &= ~DCF_SEEKABLE;
|
flags &= ~(DCF_SEEKABLE | DCF_CACHED);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -980,7 +980,14 @@ bool OpenDecompressor(FileReader& self, FileReader &parent, FileReader::Size len
|
||||||
}
|
}
|
||||||
dec->Length = length;
|
dec->Length = length;
|
||||||
}
|
}
|
||||||
if ((flags & DCF_SEEKABLE))
|
if ((flags & DCF_CACHED))
|
||||||
|
{
|
||||||
|
// read everything into a MemoryArrayReader.
|
||||||
|
FileData data(nullptr, length);
|
||||||
|
fr->Read(data.writable(), length);
|
||||||
|
fr = new MemoryArrayReader(data);
|
||||||
|
}
|
||||||
|
else if ((flags & DCF_SEEKABLE))
|
||||||
{
|
{
|
||||||
// create a wrapper that can buffer the content so that seeking is possible
|
// create a wrapper that can buffer the content so that seeking is possible
|
||||||
fr = new BufferingReader(fr);
|
fr = new BufferingReader(fr);
|
||||||
|
|
|
@ -396,7 +396,7 @@ void FileSystem::AddFile (const char *filename, FileReader *filer, LumpFilterInf
|
||||||
std::string path = filename;
|
std::string path = filename;
|
||||||
path += ':';
|
path += ':';
|
||||||
path += resfile->getName(i);
|
path += resfile->getName(i);
|
||||||
auto embedded = resfile->GetEntryReader(i, true);
|
auto embedded = resfile->GetEntryReader(i, READER_NEW, READERFLAG_SEEKABLE);
|
||||||
AddFile(path.c_str(), &embedded, filter, Printf, hashfile);
|
AddFile(path.c_str(), &embedded, filter, Printf, hashfile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -428,7 +428,7 @@ void FileSystem::AddFile (const char *filename, FileReader *filer, LumpFilterInf
|
||||||
int flags = resfile->GetEntryFlags(i);
|
int flags = resfile->GetEntryFlags(i);
|
||||||
if (!(flags & RESFF_EMBEDDED))
|
if (!(flags & RESFF_EMBEDDED))
|
||||||
{
|
{
|
||||||
auto reader = resfile->GetEntryReader(i, true);
|
auto reader = resfile->GetEntryReader(i, READER_SHARED, 0);
|
||||||
md5Hash(filereader, cksum);
|
md5Hash(filereader, cksum);
|
||||||
|
|
||||||
for (size_t j = 0; j < sizeof(cksum); ++j)
|
for (size_t j = 0; j < sizeof(cksum); ++j)
|
||||||
|
@ -1306,7 +1306,7 @@ FileData FileSystem::ReadFile (int lump)
|
||||||
//==========================================================================
|
//==========================================================================
|
||||||
|
|
||||||
|
|
||||||
FileReader FileSystem::OpenFileReader(int lump)
|
FileReader FileSystem::OpenFileReader(int lump, int readertype, int readerflags)
|
||||||
{
|
{
|
||||||
if ((unsigned)lump >= (unsigned)FileInfo.size())
|
if ((unsigned)lump >= (unsigned)FileInfo.size())
|
||||||
{
|
{
|
||||||
|
@ -1314,18 +1314,7 @@ FileReader FileSystem::OpenFileReader(int lump)
|
||||||
}
|
}
|
||||||
|
|
||||||
auto file = FileInfo[lump].resfile;
|
auto file = FileInfo[lump].resfile;
|
||||||
return file->GetEntryReader(FileInfo[lump].resindex, false);
|
return file->GetEntryReader(FileInfo[lump].resindex, readertype, readerflags);
|
||||||
}
|
|
||||||
|
|
||||||
FileReader FileSystem::ReopenFileReader(int lump, bool alwayscache)
|
|
||||||
{
|
|
||||||
if ((unsigned)lump >= (unsigned)FileInfo.size())
|
|
||||||
{
|
|
||||||
throw FileSystemException("ReopenFileReader: %u >= NumEntries", lump);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto file = FileInfo[lump].resfile;
|
|
||||||
return file->GetEntryReader(FileInfo[lump].resindex, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FileReader FileSystem::OpenFileReader(const char* name)
|
FileReader FileSystem::OpenFileReader(const char* name)
|
||||||
|
|
|
@ -211,7 +211,7 @@ FCompressedBuffer FResourceFile::GetRawData(uint32_t entry)
|
||||||
FCompressedBuffer cbuf = { LumpSize, LumpSize, METHOD_STORED, 0, 0, LumpSize == 0? nullptr : new char[LumpSize] };
|
FCompressedBuffer cbuf = { LumpSize, LumpSize, METHOD_STORED, 0, 0, LumpSize == 0? nullptr : new char[LumpSize] };
|
||||||
if (LumpSize > 0)
|
if (LumpSize > 0)
|
||||||
{
|
{
|
||||||
auto fr = GetEntryReader(entry);
|
auto fr = GetEntryReader(entry, READER_SHARED, 0);
|
||||||
size_t read = fr.Read(cbuf.mBuffer, LumpSize);
|
size_t read = fr.Read(cbuf.mBuffer, LumpSize);
|
||||||
if (read < LumpSize)
|
if (read < LumpSize)
|
||||||
{
|
{
|
||||||
|
@ -542,7 +542,7 @@ int FResourceFile::FindEntry(const char *name)
|
||||||
//
|
//
|
||||||
//==========================================================================
|
//==========================================================================
|
||||||
|
|
||||||
FileReader FResourceFile::GetEntryReader(uint32_t entry, bool newreader)
|
FileReader FResourceFile::GetEntryReader(uint32_t entry, int readertype, int readerflags)
|
||||||
{
|
{
|
||||||
FileReader fr;
|
FileReader fr;
|
||||||
if (entry < NumLumps)
|
if (entry < NumLumps)
|
||||||
|
@ -553,20 +553,30 @@ FileReader FResourceFile::GetEntryReader(uint32_t entry, bool newreader)
|
||||||
}
|
}
|
||||||
if (!(Entries[entry].Flags & RESFF_COMPRESSED))
|
if (!(Entries[entry].Flags & RESFF_COMPRESSED))
|
||||||
{
|
{
|
||||||
if (!newreader)
|
if (readertype == READER_SHARED)
|
||||||
{
|
{
|
||||||
fr.OpenFilePart(Reader, Entries[entry].Position, Entries[entry].Length);
|
fr.OpenFilePart(Reader, Entries[entry].Position, Entries[entry].Length);
|
||||||
}
|
}
|
||||||
else
|
else if (readertype == READER_NEW)
|
||||||
{
|
{
|
||||||
fr.OpenFile(FileName, Entries[entry].Position, Entries[entry].Length);
|
fr.OpenFile(FileName, Entries[entry].Position, Entries[entry].Length);
|
||||||
}
|
}
|
||||||
|
else if (readertype == READER_CACHED)
|
||||||
|
{
|
||||||
|
Reader.Seek(Entries[entry].Position, FileReader::SeekSet);
|
||||||
|
auto data = Reader.Read(Entries[entry].Length);
|
||||||
|
fr.OpenMemoryArray(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
FileReader fri;
|
FileReader fri;
|
||||||
fri.OpenFilePart(Reader, Entries[entry].Position, Entries[entry].CompressedSize);
|
if (readertype == READER_NEW) fri.OpenFile(FileName, Entries[entry].Position, Entries[entry].CompressedSize);
|
||||||
OpenDecompressor(fr, fri, Entries[entry].Length, Entries[entry].Method, FileSys::DCF_TRANSFEROWNER | FileSys::DCF_SEEKABLE | FileSys::DCF_EXCEPTIONS);
|
else fri.OpenFilePart(Reader, Entries[entry].Position, Entries[entry].CompressedSize);
|
||||||
|
int flags = DCF_TRANSFEROWNER | DCF_EXCEPTIONS;
|
||||||
|
if (readertype == READER_CACHED) flags |= DCF_CACHED;
|
||||||
|
else if (readerflags & READERFLAG_SEEKABLE) flags |= DCF_SEEKABLE;
|
||||||
|
OpenDecompressor(fr, fri, Entries[entry].Length, Entries[entry].Method, flags);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fr;
|
return fr;
|
||||||
|
|
|
@ -316,7 +316,7 @@ unsigned FSavegameManagerBase::ExtractSaveData(int index)
|
||||||
auto pic = resf->FindEntry("savepic.png");
|
auto pic = resf->FindEntry("savepic.png");
|
||||||
if (pic >= 0)
|
if (pic >= 0)
|
||||||
{
|
{
|
||||||
FileReader picreader = resf->GetEntryReader(pic, true);
|
FileReader picreader = resf->GetEntryReader(pic, FileSys::READER_NEW, FileSys::READERFLAG_SEEKABLE);
|
||||||
PNGHandle *png = M_VerifyPNG(picreader);
|
PNGHandle *png = M_VerifyPNG(picreader);
|
||||||
if (png != nullptr)
|
if (png != nullptr)
|
||||||
{
|
{
|
||||||
|
|
|
@ -798,7 +798,7 @@ static int FindGLNodesInFile(FResourceFile * f, const char * label)
|
||||||
if (mustcheck)
|
if (mustcheck)
|
||||||
{
|
{
|
||||||
char check[16]={0};
|
char check[16]={0};
|
||||||
auto fr = f->GetEntryReader(i, false);
|
auto fr = f->GetEntryReader(i, FileSys::READER_SHARED);
|
||||||
fr.Read(check, 16);
|
fr.Read(check, 16);
|
||||||
if (MatchHeader(label, check)) return i;
|
if (MatchHeader(label, check)) return i;
|
||||||
}
|
}
|
||||||
|
@ -906,7 +906,7 @@ bool MapLoader::LoadGLNodes(MapData * map)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
gwalumps[i] = f_gwa->GetEntryReader(li + i + 1);
|
gwalumps[i] = f_gwa->GetEntryReader(li + i + 1, FileSys::READER_NEW, FileSys::READERFLAG_SEEKABLE);
|
||||||
}
|
}
|
||||||
if (result) result = DoLoadGLNodes(gwalumps);
|
if (result) result = DoLoadGLNodes(gwalumps);
|
||||||
}
|
}
|
||||||
|
|
|
@ -280,7 +280,7 @@ MapData *P_OpenMapData(const char * mapname, bool justcheck)
|
||||||
char maplabel[9]="";
|
char maplabel[9]="";
|
||||||
int index=0;
|
int index=0;
|
||||||
|
|
||||||
map->MapLumps[0].Reader = map->resource->GetEntryReader(0);
|
map->MapLumps[0].Reader = map->resource->GetEntryReader(0, FileSys::READER_SHARED);
|
||||||
uppercopy(map->MapLumps[0].Name, map->resource->getName(0));
|
uppercopy(map->MapLumps[0].Name, map->resource->getName(0));
|
||||||
|
|
||||||
for(uint32_t i = 1; i < map->resource->EntryCount(); i++)
|
for(uint32_t i = 1; i < map->resource->EntryCount(); i++)
|
||||||
|
@ -290,7 +290,7 @@ MapData *P_OpenMapData(const char * mapname, bool justcheck)
|
||||||
if (i == 1 && !strnicmp(lumpname, "TEXTMAP", 8))
|
if (i == 1 && !strnicmp(lumpname, "TEXTMAP", 8))
|
||||||
{
|
{
|
||||||
map->isText = true;
|
map->isText = true;
|
||||||
map->MapLumps[ML_TEXTMAP].Reader = map->resource->GetEntryReader(i);
|
map->MapLumps[ML_TEXTMAP].Reader = map->resource->GetEntryReader(i, FileSys::READER_SHARED);
|
||||||
strncpy(map->MapLumps[ML_TEXTMAP].Name, lumpname, 8);
|
strncpy(map->MapLumps[ML_TEXTMAP].Name, lumpname, 8);
|
||||||
for(int i = 2;; i++)
|
for(int i = 2;; i++)
|
||||||
{
|
{
|
||||||
|
@ -326,7 +326,7 @@ MapData *P_OpenMapData(const char * mapname, bool justcheck)
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
else continue;
|
else continue;
|
||||||
map->MapLumps[index].Reader = map->resource->GetEntryReader(i);
|
map->MapLumps[index].Reader = map->resource->GetEntryReader(i, FileSys::READER_SHARED);
|
||||||
strncpy(map->MapLumps[index].Name, lumpname, 8);
|
strncpy(map->MapLumps[index].Name, lumpname, 8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -358,7 +358,7 @@ MapData *P_OpenMapData(const char * mapname, bool justcheck)
|
||||||
maplabel[8]=0;
|
maplabel[8]=0;
|
||||||
}
|
}
|
||||||
|
|
||||||
map->MapLumps[index].Reader = map->resource->GetEntryReader(i);
|
map->MapLumps[index].Reader = map->resource->GetEntryReader(i, FileSys::READER_SHARED);
|
||||||
strncpy(map->MapLumps[index].Name, lumpname, 8);
|
strncpy(map->MapLumps[index].Name, lumpname, 8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue