mirror of https://github.com/PCSX2/pcsx2.git
Merge pull request #757 from AdmiralCurtiss/folder-memcard-keep-files-open
FolderMemcard: Keep open handles on files.
This commit is contained in:
commit
f9e6a02346
|
@ -43,7 +43,7 @@ void FolderMemoryCard::InitializeInternalData() {
|
||||||
m_timeLastWritten = 0;
|
m_timeLastWritten = 0;
|
||||||
m_isEnabled = false;
|
m_isEnabled = false;
|
||||||
m_framesUntilFlush = 0;
|
m_framesUntilFlush = 0;
|
||||||
m_lastAccessedFile.Close();
|
m_lastAccessedFile.CloseAll();
|
||||||
m_performFileWrites = true;
|
m_performFileWrites = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ void FolderMemoryCard::Close( bool flush ) {
|
||||||
m_cache.clear();
|
m_cache.clear();
|
||||||
m_oldDataCache.clear();
|
m_oldDataCache.clear();
|
||||||
m_fileMetadataQuickAccess.clear();
|
m_fileMetadataQuickAccess.clear();
|
||||||
m_lastAccessedFile.Close();
|
m_lastAccessedFile.CloseAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FolderMemoryCard::LoadMemoryCardData( const u32 sizeInClusters, const bool enableFiltering, const wxString& filter ) {
|
void FolderMemoryCard::LoadMemoryCardData( const u32 sizeInClusters, const bool enableFiltering, const wxString& filter ) {
|
||||||
|
@ -455,7 +455,11 @@ bool FolderMemoryCard::AddFile( MemoryCardFileEntry* const dirEntry, const wxStr
|
||||||
|
|
||||||
file.Close();
|
file.Close();
|
||||||
|
|
||||||
AddFileEntryToMetadataQuickAccess( newFileEntry, parent );
|
MemoryCardFileMetadataReference* fileRef = AddFileEntryToMetadataQuickAccess( newFileEntry, parent );
|
||||||
|
if ( fileRef != nullptr ) {
|
||||||
|
// acquire a handle on the file so nothing else can change the file contents while the memory card is open
|
||||||
|
m_lastAccessedFile.ReOpen( m_folderName, fileRef );
|
||||||
|
}
|
||||||
|
|
||||||
// and finally, increase file count in the directory entry
|
// and finally, increase file count in the directory entry
|
||||||
dirEntry->entry.data.length++;
|
dirEntry->entry.data.length++;
|
||||||
|
@ -508,12 +512,13 @@ MemoryCardFileMetadataReference* FolderMemoryCard::AddDirEntryToMetadataQuickAcc
|
||||||
return ref;
|
return ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FolderMemoryCard::AddFileEntryToMetadataQuickAccess( MemoryCardFileEntry* const entry, MemoryCardFileMetadataReference* const parent ) {
|
MemoryCardFileMetadataReference* FolderMemoryCard::AddFileEntryToMetadataQuickAccess( MemoryCardFileEntry* const entry, MemoryCardFileMetadataReference* const parent ) {
|
||||||
u32 fileCluster = entry->entry.data.cluster;
|
const u32 firstFileCluster = entry->entry.data.cluster;
|
||||||
|
u32 fileCluster = firstFileCluster;
|
||||||
|
|
||||||
// zero-length files have no file clusters
|
// zero-length files have no file clusters
|
||||||
if ( fileCluster == 0xFFFFFFFFu ) {
|
if ( fileCluster == 0xFFFFFFFFu ) {
|
||||||
return;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 clusterNumber = 0;
|
u32 clusterNumber = 0;
|
||||||
|
@ -524,6 +529,8 @@ void FolderMemoryCard::AddFileEntryToMetadataQuickAccess( MemoryCardFileEntry* c
|
||||||
ref->consecutiveCluster = clusterNumber;
|
ref->consecutiveCluster = clusterNumber;
|
||||||
++clusterNumber;
|
++clusterNumber;
|
||||||
} while ( ( fileCluster = m_fat.data[0][0][fileCluster] ) != ( LastDataCluster | DataClusterInUseMask ) );
|
} while ( ( fileCluster = m_fat.data[0][0][fileCluster] ) != ( LastDataCluster | DataClusterInUseMask ) );
|
||||||
|
|
||||||
|
return &m_fileMetadataQuickAccess[firstFileCluster & NextDataClusterMask];
|
||||||
}
|
}
|
||||||
|
|
||||||
s32 FolderMemoryCard::IsPresent() const {
|
s32 FolderMemoryCard::IsPresent() const {
|
||||||
|
@ -687,7 +694,7 @@ bool FolderMemoryCard::ReadFromFile( u8 *dest, u32 adr, u32 dataLength ) {
|
||||||
auto it = m_fileMetadataQuickAccess.find( fatCluster );
|
auto it = m_fileMetadataQuickAccess.find( fatCluster );
|
||||||
if ( it != m_fileMetadataQuickAccess.end() ) {
|
if ( it != m_fileMetadataQuickAccess.end() ) {
|
||||||
const u32 clusterNumber = it->second.consecutiveCluster;
|
const u32 clusterNumber = it->second.consecutiveCluster;
|
||||||
wxFFile* file = m_lastAccessedFile.ReOpen( m_folderName, &it->second, L"rb" );
|
wxFFile* file = m_lastAccessedFile.ReOpen( m_folderName, &it->second );
|
||||||
if ( file->IsOpened() ) {
|
if ( file->IsOpened() ) {
|
||||||
const u32 clusterOffset = ( page % 2 ) * PageSize + offset;
|
const u32 clusterOffset = ( page % 2 ) * PageSize + offset;
|
||||||
const u32 fileOffset = clusterNumber * ClusterSize + clusterOffset;
|
const u32 fileOffset = clusterNumber * ClusterSize + clusterOffset;
|
||||||
|
@ -821,7 +828,6 @@ void FolderMemoryCard::NextFrame() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void FolderMemoryCard::Flush() {
|
void FolderMemoryCard::Flush() {
|
||||||
m_lastAccessedFile.Close();
|
|
||||||
if ( m_cache.empty() ) { return; }
|
if ( m_cache.empty() ) { return; }
|
||||||
|
|
||||||
Console.WriteLn( L"(FolderMcd) Writing data for slot %u to file system...", m_slot );
|
Console.WriteLn( L"(FolderMcd) Writing data for slot %u to file system...", m_slot );
|
||||||
|
@ -877,7 +883,7 @@ void FolderMemoryCard::Flush() {
|
||||||
FlushPage( i );
|
FlushPage( i );
|
||||||
}
|
}
|
||||||
|
|
||||||
m_lastAccessedFile.Close();
|
m_lastAccessedFile.FlushAll();
|
||||||
m_oldDataCache.clear();
|
m_oldDataCache.clear();
|
||||||
|
|
||||||
const u64 timeFlushEnd = wxGetLocalTimeMillis().GetValue();
|
const u64 timeFlushEnd = wxGetLocalTimeMillis().GetValue();
|
||||||
|
@ -1005,6 +1011,7 @@ void FolderMemoryCard::FlushDeletedFilesAndRemoveUnchangedDataFromCache( const s
|
||||||
FileAccessHelper::CleanMemcardFilename( cleanName );
|
FileAccessHelper::CleanMemcardFilename( cleanName );
|
||||||
const wxString fileName = wxString::FromAscii( cleanName );
|
const wxString fileName = wxString::FromAscii( cleanName );
|
||||||
const wxString filePath = m_folderName.GetFullPath() + dirPath + L"/" + fileName;
|
const wxString filePath = m_folderName.GetFullPath() + dirPath + L"/" + fileName;
|
||||||
|
m_lastAccessedFile.CloseMatching( filePath );
|
||||||
const wxString newFilePath = m_folderName.GetFullPath() + dirPath + L"/_pcsx2_deleted_" + fileName;
|
const wxString newFilePath = m_folderName.GetFullPath() + dirPath + L"/_pcsx2_deleted_" + fileName;
|
||||||
if ( wxFileName::DirExists( newFilePath ) ) {
|
if ( wxFileName::DirExists( newFilePath ) ) {
|
||||||
// wxRenameFile doesn't overwrite directories, so we have to remove the old one first
|
// wxRenameFile doesn't overwrite directories, so we have to remove the old one first
|
||||||
|
@ -1111,7 +1118,7 @@ bool FolderMemoryCard::WriteToFile( const u8* src, u32 adr, u32 dataLength ) {
|
||||||
const u32 clusterNumber = it->second.consecutiveCluster;
|
const u32 clusterNumber = it->second.consecutiveCluster;
|
||||||
|
|
||||||
if ( m_performFileWrites ) {
|
if ( m_performFileWrites ) {
|
||||||
wxFFile* file = m_lastAccessedFile.ReOpen( m_folderName, &it->second, L"r+b", true );
|
wxFFile* file = m_lastAccessedFile.ReOpen( m_folderName, &it->second, true );
|
||||||
if ( file->IsOpened() ) {
|
if ( file->IsOpened() ) {
|
||||||
const u32 clusterOffset = ( page % 2 ) * PageSize + offset;
|
const u32 clusterOffset = ( page % 2 ) * PageSize + offset;
|
||||||
const u32 fileSize = entry->entry.data.length;
|
const u32 fileSize = entry->entry.data.length;
|
||||||
|
@ -1309,16 +1316,16 @@ void FolderMemoryCard::CalculateECC( u8* ecc, const u8* data ) {
|
||||||
|
|
||||||
|
|
||||||
FileAccessHelper::FileAccessHelper() {
|
FileAccessHelper::FileAccessHelper() {
|
||||||
m_file = nullptr;
|
m_files.clear();
|
||||||
|
m_lastWrittenFileRef = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
FileAccessHelper::~FileAccessHelper() {
|
FileAccessHelper::~FileAccessHelper() {
|
||||||
this->Close();
|
m_lastWrittenFileRef = nullptr;
|
||||||
|
this->CloseAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
wxFFile* FileAccessHelper::Open( const wxFileName& folderName, MemoryCardFileMetadataReference* fileRef, const wxString& mode, bool writeMetadata ) {
|
wxFFile* FileAccessHelper::Open( const wxFileName& folderName, MemoryCardFileMetadataReference* fileRef, bool writeMetadata ) {
|
||||||
this->Close();
|
|
||||||
|
|
||||||
wxFileName fn( folderName );
|
wxFileName fn( folderName );
|
||||||
bool cleanedFilename = fileRef->GetPath( &fn );
|
bool cleanedFilename = fileRef->GetPath( &fn );
|
||||||
wxString filename( fn.GetFullPath() );
|
wxString filename( fn.GetFullPath() );
|
||||||
|
@ -1331,54 +1338,114 @@ wxFFile* FileAccessHelper::Open( const wxFileName& folderName, MemoryCardFileMet
|
||||||
createEmptyFile.Close();
|
createEmptyFile.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_file = new wxFFile( filename, mode );
|
const MemoryCardFileEntry* const entry = fileRef->entry;
|
||||||
m_entry = fileRef->entry;
|
wxFFile* file = new wxFFile( filename, L"r+b" );
|
||||||
m_mode = mode;
|
m_files.emplace( entry, file );
|
||||||
|
|
||||||
if ( writeMetadata ) {
|
if ( writeMetadata ) {
|
||||||
const MemoryCardFileEntry* const entry = fileRef->entry;
|
|
||||||
|
|
||||||
// write metadata of file if it's nonstandard
|
|
||||||
fn.AppendDir( L"_pcsx2_meta" );
|
fn.AppendDir( L"_pcsx2_meta" );
|
||||||
if ( cleanedFilename || entry->entry.data.mode != MemoryCardFileEntry::DefaultFileMode || entry->entry.data.attr != 0 ) {
|
const bool metadataIsNonstandard = cleanedFilename || entry->entry.data.mode != MemoryCardFileEntry::DefaultFileMode || entry->entry.data.attr != 0;
|
||||||
if ( !fn.DirExists() ) {
|
WriteMetadata( metadataIsNonstandard, fn, entry );
|
||||||
fn.Mkdir();
|
|
||||||
}
|
}
|
||||||
wxFFile metaFile( fn.GetFullPath(), L"wb" );
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileAccessHelper::WriteMetadata( const wxFileName& folderName, MemoryCardFileMetadataReference* fileRef ) {
|
||||||
|
wxFileName fn( folderName );
|
||||||
|
bool cleanedFilename = fileRef->GetPath( &fn );
|
||||||
|
fn.AppendDir( L"_pcsx2_meta" );
|
||||||
|
|
||||||
|
const MemoryCardFileEntry* const entry = fileRef->entry;
|
||||||
|
const bool metadataIsNonstandard = cleanedFilename || entry->entry.data.mode != MemoryCardFileEntry::DefaultFileMode || entry->entry.data.attr != 0;
|
||||||
|
|
||||||
|
WriteMetadata( metadataIsNonstandard, fn, entry );
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileAccessHelper::WriteMetadata( bool metadataIsNonstandard, const wxFileName& metadataFilename, const MemoryCardFileEntry* const entry ) {
|
||||||
|
if ( metadataIsNonstandard ) {
|
||||||
|
// write metadata of file if it's nonstandard
|
||||||
|
if ( !metadataFilename.DirExists() ) {
|
||||||
|
metadataFilename.Mkdir();
|
||||||
|
}
|
||||||
|
wxFFile metaFile( metadataFilename.GetFullPath(), L"wb" );
|
||||||
if ( metaFile.IsOpened() ) {
|
if ( metaFile.IsOpened() ) {
|
||||||
metaFile.Write( entry->entry.raw, sizeof( entry->entry.raw ) );
|
metaFile.Write( entry->entry.raw, sizeof( entry->entry.raw ) );
|
||||||
metaFile.Close();
|
metaFile.Close();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if metadata is standard remove metadata file if it exists
|
// if metadata is standard remove metadata file if it exists
|
||||||
if ( fn.FileExists() ) {
|
if ( metadataFilename.FileExists() ) {
|
||||||
wxRemoveFile( fn.GetFullPath() );
|
wxRemoveFile( metadataFilename.GetFullPath() );
|
||||||
|
|
||||||
// and remove the metadata dir if it's now empty
|
// and remove the metadata dir if it's now empty
|
||||||
wxDir metaDir( fn.GetPath() );
|
wxDir metaDir( metadataFilename.GetPath() );
|
||||||
if ( metaDir.IsOpened() && !metaDir.HasFiles() ) {
|
if ( metaDir.IsOpened() && !metaDir.HasFiles() ) {
|
||||||
wxRmdir( fn.GetPath() );
|
wxRmdir( metadataFilename.GetPath() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return m_file;
|
wxFFile* FileAccessHelper::ReOpen( const wxFileName& folderName, MemoryCardFileMetadataReference* fileRef, bool writeMetadata ) {
|
||||||
}
|
auto it = m_files.find( fileRef->entry );
|
||||||
|
if ( it != m_files.end() ) {
|
||||||
|
// we already have a handle to this file
|
||||||
|
|
||||||
wxFFile* FileAccessHelper::ReOpen( const wxFileName& folderName, MemoryCardFileMetadataReference* fileRef, const wxString& mode, bool writeMetadata ) {
|
// if the caller wants to write metadata and we haven't done this recently, do so and remember that we did
|
||||||
if ( m_file && fileRef->entry == m_entry && mode == m_mode ) {
|
if ( writeMetadata ) {
|
||||||
return m_file;
|
if ( m_lastWrittenFileRef != fileRef ) {
|
||||||
|
WriteMetadata( folderName, fileRef );
|
||||||
|
m_lastWrittenFileRef = fileRef;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return this->Open( folderName, fileRef, mode, writeMetadata );
|
if ( m_lastWrittenFileRef != nullptr ) {
|
||||||
|
m_lastWrittenFileRef = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileAccessHelper::Close() {
|
return it->second;
|
||||||
if ( m_file ) {
|
} else {
|
||||||
m_file->Close();
|
return this->Open( folderName, fileRef, writeMetadata );
|
||||||
delete m_file;
|
}
|
||||||
m_file = nullptr;
|
}
|
||||||
|
|
||||||
|
void FileAccessHelper::CloseFileHandle( wxFFile* file, const MemoryCardFileEntry* entry ) {
|
||||||
|
file->Close();
|
||||||
|
|
||||||
|
if ( entry != nullptr ) {
|
||||||
|
wxFileName fn( file->GetName() );
|
||||||
|
fn.SetTimes( nullptr, &entry->entry.data.timeModified.ToWxDateTime(), &entry->entry.data.timeCreated.ToWxDateTime() );
|
||||||
|
}
|
||||||
|
|
||||||
|
delete file;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileAccessHelper::CloseMatching( const wxString& path ) {
|
||||||
|
wxFileName fn( path );
|
||||||
|
fn.Normalize();
|
||||||
|
wxString pathNormalized = fn.GetFullPath();
|
||||||
|
for ( auto it = m_files.begin(); it != m_files.end(); ) {
|
||||||
|
wxString openPath = it->second->GetName();
|
||||||
|
if ( openPath.StartsWith( pathNormalized ) ) {
|
||||||
|
CloseFileHandle( it->second, it->first );
|
||||||
|
it = m_files.erase( it );
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileAccessHelper::CloseAll() {
|
||||||
|
for ( auto it = m_files.begin(); it != m_files.end(); ++it ) {
|
||||||
|
CloseFileHandle( it->second, it->first );
|
||||||
|
}
|
||||||
|
m_files.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileAccessHelper::FlushAll() {
|
||||||
|
for ( auto it = m_files.begin(); it != m_files.end(); ++it ) {
|
||||||
|
it->second->Flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,19 @@ struct MemoryCardFileEntryDateTime {
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wxDateTime ToWxDateTime() const {
|
||||||
|
wxDateTime::Tm tm;
|
||||||
|
tm.sec = this->second;
|
||||||
|
tm.min = this->minute;
|
||||||
|
tm.hour = this->hour;
|
||||||
|
tm.mday = this->day;
|
||||||
|
tm.mon = (wxDateTime::Month)(this->month - 1);
|
||||||
|
tm.year = this->year;
|
||||||
|
|
||||||
|
wxDateTime time( tm );
|
||||||
|
return time.FromTimezone( wxDateTime::GMT9 );
|
||||||
|
}
|
||||||
|
|
||||||
bool operator==( const MemoryCardFileEntryDateTime& other ) const {
|
bool operator==( const MemoryCardFileEntryDateTime& other ) const {
|
||||||
return unused == other.unused && second == other.second && minute == other.minute && hour == other.hour
|
return unused == other.unused && second == other.second && minute == other.minute && hour == other.hour
|
||||||
&& day == other.day && month == other.month && year == other.year;
|
&& day == other.day && month == other.month && year == other.year;
|
||||||
|
@ -187,18 +200,21 @@ struct MemoryCardFileMetadataReference {
|
||||||
// Small helper class to keep memory card files opened between calls to Read()/Save()
|
// Small helper class to keep memory card files opened between calls to Read()/Save()
|
||||||
class FileAccessHelper {
|
class FileAccessHelper {
|
||||||
protected:
|
protected:
|
||||||
wxFFile* m_file;
|
std::map<const MemoryCardFileEntry* const, wxFFile*> m_files;
|
||||||
const MemoryCardFileEntry* m_entry;
|
MemoryCardFileMetadataReference* m_lastWrittenFileRef; // we remember this to reduce redundant metadata checks/writes
|
||||||
wxString m_mode;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
FileAccessHelper();
|
FileAccessHelper();
|
||||||
~FileAccessHelper();
|
~FileAccessHelper();
|
||||||
|
|
||||||
// Get an already opened file if possible, or open a new one and remember it
|
// Get an already opened file if possible, or open a new one and remember it
|
||||||
wxFFile* ReOpen( const wxFileName& folderName, MemoryCardFileMetadataReference* fileRef, const wxString& mode, bool writeMetadata = false );
|
wxFFile* ReOpen( const wxFileName& folderName, MemoryCardFileMetadataReference* fileRef, bool writeMetadata = false );
|
||||||
// Close an open file, if any
|
// Close all open files that start with the given path, so either a file if a filename is given or all files in a directory and its subdirectories when a directory is given
|
||||||
void Close();
|
void CloseMatching( const wxString& path );
|
||||||
|
// Close all open files
|
||||||
|
void CloseAll();
|
||||||
|
// Flush the written data of all open files to the file system
|
||||||
|
void FlushAll();
|
||||||
|
|
||||||
// removes characters from a PS2 file name that would be illegal in a Windows file system
|
// removes characters from a PS2 file name that would be illegal in a Windows file system
|
||||||
// returns true if any changes were made
|
// returns true if any changes were made
|
||||||
|
@ -206,7 +222,13 @@ public:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Open a new file and remember it for later
|
// Open a new file and remember it for later
|
||||||
wxFFile* Open( const wxFileName& folderName, MemoryCardFileMetadataReference* fileRef, const wxString& mode, bool writeMetadata = false );
|
wxFFile* Open( const wxFileName& folderName, MemoryCardFileMetadataReference* fileRef, bool writeMetadata = false );
|
||||||
|
// Close a file and delete its handle
|
||||||
|
// If entry is given, it also attempts to set the created and modified timestamps of the file according to the entry
|
||||||
|
void CloseFileHandle( wxFFile* file, const MemoryCardFileEntry* entry = nullptr );
|
||||||
|
|
||||||
|
void WriteMetadata( const wxFileName& folderName, MemoryCardFileMetadataReference* fileRef );
|
||||||
|
void WriteMetadata( bool metadataIsNonstandard, const wxFileName& metadataFilename, const MemoryCardFileEntry* const entry );
|
||||||
};
|
};
|
||||||
|
|
||||||
// --------------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------------
|
||||||
|
@ -414,7 +436,8 @@ protected:
|
||||||
|
|
||||||
|
|
||||||
// adds a file to the quick-access dictionary, so it can be accessed more efficiently (ie, without searching through the entire file system) later
|
// adds a file to the quick-access dictionary, so it can be accessed more efficiently (ie, without searching through the entire file system) later
|
||||||
void AddFileEntryToMetadataQuickAccess( MemoryCardFileEntry* const entry, MemoryCardFileMetadataReference* const parent );
|
// returns the MemoryCardFileMetadataReference of the first file cluster, or nullptr if the file is zero-length
|
||||||
|
MemoryCardFileMetadataReference* AddFileEntryToMetadataQuickAccess( MemoryCardFileEntry* const entry, MemoryCardFileMetadataReference* const parent );
|
||||||
|
|
||||||
// creates a reference to a directory entry, so it can be passed as parent to other files/directories
|
// creates a reference to a directory entry, so it can be passed as parent to other files/directories
|
||||||
MemoryCardFileMetadataReference* AddDirEntryToMetadataQuickAccess( MemoryCardFileEntry* const entry, MemoryCardFileMetadataReference* const parent );
|
MemoryCardFileMetadataReference* AddDirEntryToMetadataQuickAccess( MemoryCardFileEntry* const entry, MemoryCardFileMetadataReference* const parent );
|
||||||
|
|
Loading…
Reference in New Issue