Level save + load
This commit is contained in:
543
Assets/Plugins/SimpleFileBrowser/Scripts/FileBrowserHelpers.cs
Normal file
543
Assets/Plugins/SimpleFileBrowser/Scripts/FileBrowserHelpers.cs
Normal file
@@ -0,0 +1,543 @@
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SimpleFileBrowser
|
||||
{
|
||||
public struct FileSystemEntry
|
||||
{
|
||||
public readonly string Path;
|
||||
public readonly string Name;
|
||||
public readonly string Extension;
|
||||
public readonly FileAttributes Attributes;
|
||||
|
||||
public bool IsDirectory { get { return ( Attributes & FileAttributes.Directory ) == FileAttributes.Directory; } }
|
||||
|
||||
public FileSystemEntry( string path, string name, string extension, bool isDirectory )
|
||||
{
|
||||
Path = path;
|
||||
Name = name;
|
||||
Extension = extension;
|
||||
Attributes = isDirectory ? FileAttributes.Directory : FileAttributes.Normal;
|
||||
}
|
||||
|
||||
public FileSystemEntry( FileSystemInfo fileInfo, string extension )
|
||||
{
|
||||
Path = fileInfo.FullName;
|
||||
Name = fileInfo.Name;
|
||||
Extension = extension;
|
||||
Attributes = fileInfo.Attributes;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileBrowserHelpers
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
private static AndroidJavaClass m_ajc = null;
|
||||
public static AndroidJavaClass AJC
|
||||
{
|
||||
get
|
||||
{
|
||||
if( m_ajc == null )
|
||||
m_ajc = new AndroidJavaClass( "com.yasirkula.unity.FileBrowser" );
|
||||
|
||||
return m_ajc;
|
||||
}
|
||||
}
|
||||
|
||||
private static AndroidJavaObject m_context = null;
|
||||
public static AndroidJavaObject Context
|
||||
{
|
||||
get
|
||||
{
|
||||
if( m_context == null )
|
||||
{
|
||||
using( AndroidJavaObject unityClass = new AndroidJavaClass( "com.unity3d.player.UnityPlayer" ) )
|
||||
{
|
||||
m_context = unityClass.GetStatic<AndroidJavaObject>( "currentActivity" );
|
||||
}
|
||||
}
|
||||
|
||||
return m_context;
|
||||
}
|
||||
}
|
||||
|
||||
private static string m_temporaryFilePath = null;
|
||||
private static string TemporaryFilePath
|
||||
{
|
||||
get
|
||||
{
|
||||
if( m_temporaryFilePath == null )
|
||||
{
|
||||
m_temporaryFilePath = Path.Combine( Application.temporaryCachePath, "tmpFile" );
|
||||
Directory.CreateDirectory( Application.temporaryCachePath );
|
||||
}
|
||||
|
||||
return m_temporaryFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
// On Android 10+, filesystem can be accessed via Storage Access Framework only
|
||||
private static bool? m_shouldUseSAF = null;
|
||||
public static bool ShouldUseSAF
|
||||
{
|
||||
get
|
||||
{
|
||||
if( m_shouldUseSAF == null )
|
||||
m_shouldUseSAF = AJC.CallStatic<bool>( "CheckSAF" );
|
||||
|
||||
return m_shouldUseSAF.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ShouldUseSAFForPath( string path ) // true: path should be managed with AJC (native helper class for Storage Access Framework), false: path should be managed with System.IO
|
||||
{
|
||||
return ShouldUseSAF && ( string.IsNullOrEmpty( path ) || path[0] != '/' );
|
||||
}
|
||||
#endif
|
||||
|
||||
public static bool FileExists( string path )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( path ) )
|
||||
return AJC.CallStatic<bool>( "SAFEntryExists", Context, path, false );
|
||||
#endif
|
||||
return File.Exists( path );
|
||||
}
|
||||
|
||||
public static bool DirectoryExists( string path )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( path ) )
|
||||
return AJC.CallStatic<bool>( "SAFEntryExists", Context, path, true );
|
||||
else if( ShouldUseSAF ) // Directory.Exists returns true even for inaccessible directories on Android 10+, we need to check if the directory is accessible
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.GetFiles( path, "testtesttest" );
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return Directory.Exists( path );
|
||||
}
|
||||
|
||||
public static bool IsDirectory( string path )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( path ) )
|
||||
return AJC.CallStatic<bool>( "SAFEntryDirectory", Context, path );
|
||||
#endif
|
||||
if( Directory.Exists( path ) )
|
||||
return true;
|
||||
if( File.Exists( path ) )
|
||||
return false;
|
||||
|
||||
string extension = Path.GetExtension( path );
|
||||
return extension == null || extension.Length <= 1; // extension includes '.'
|
||||
}
|
||||
|
||||
public static bool IsPathDescendantOfAnother( string path, string parentFolderPath )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( path ) )
|
||||
return AJC.CallStatic<bool>( "IsSAFEntryChildOfAnother", Context, path, parentFolderPath );
|
||||
#endif
|
||||
path = Path.GetFullPath( path ).Replace( '\\', '/' );
|
||||
parentFolderPath = Path.GetFullPath( parentFolderPath ).Replace( '\\', '/' );
|
||||
|
||||
if( path == parentFolderPath )
|
||||
return false;
|
||||
|
||||
if( parentFolderPath[parentFolderPath.Length - 1] != '/' )
|
||||
parentFolderPath += "/";
|
||||
|
||||
return path != parentFolderPath && path.StartsWith( parentFolderPath, System.StringComparison.OrdinalIgnoreCase );
|
||||
}
|
||||
|
||||
public static string GetDirectoryName( string path )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( path ) )
|
||||
return AJC.CallStatic<string>( "GetParentDirectory", Context, path );
|
||||
#endif
|
||||
return Path.GetDirectoryName( path );
|
||||
}
|
||||
|
||||
public static FileSystemEntry[] GetEntriesInDirectory( string path, bool extractOnlyLastSuffixFromExtensions )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( path ) )
|
||||
{
|
||||
string resultRaw = AJC.CallStatic<string>( "OpenSAFFolder", Context, path );
|
||||
int separatorIndex = resultRaw.IndexOf( "<>" );
|
||||
if( separatorIndex <= 0 )
|
||||
{
|
||||
Debug.LogError( "Entry count does not exist" );
|
||||
return null;
|
||||
}
|
||||
|
||||
int entryCount = 0;
|
||||
for( int i = 0; i < separatorIndex; i++ )
|
||||
{
|
||||
char ch = resultRaw[i];
|
||||
if( ch < '0' && ch > '9' )
|
||||
{
|
||||
Debug.LogError( "Couldn't parse entry count" );
|
||||
return null;
|
||||
}
|
||||
|
||||
entryCount = entryCount * 10 + ( ch - '0' );
|
||||
}
|
||||
|
||||
if( entryCount <= 0 )
|
||||
return null;
|
||||
|
||||
FileSystemEntry[] result = new FileSystemEntry[entryCount];
|
||||
for( int i = 0; i < entryCount; i++ )
|
||||
{
|
||||
separatorIndex += 2;
|
||||
if( separatorIndex >= resultRaw.Length )
|
||||
{
|
||||
Debug.LogError( "Couldn't fetch directory attribute" );
|
||||
return null;
|
||||
}
|
||||
|
||||
bool isDirectory = resultRaw[separatorIndex] == 'd';
|
||||
|
||||
separatorIndex++;
|
||||
int nextSeparatorIndex = resultRaw.IndexOf( "<>", separatorIndex );
|
||||
if( nextSeparatorIndex <= 0 )
|
||||
{
|
||||
Debug.LogError( "Entry name is empty" );
|
||||
return null;
|
||||
}
|
||||
|
||||
string entryName = resultRaw.Substring( separatorIndex, nextSeparatorIndex - separatorIndex );
|
||||
|
||||
separatorIndex = nextSeparatorIndex + 2;
|
||||
nextSeparatorIndex = resultRaw.IndexOf( "<>", separatorIndex );
|
||||
if( nextSeparatorIndex <= 0 )
|
||||
{
|
||||
Debug.LogError( "Entry rawUri is empty" );
|
||||
return null;
|
||||
}
|
||||
|
||||
string rawUri = resultRaw.Substring( separatorIndex, nextSeparatorIndex - separatorIndex );
|
||||
|
||||
separatorIndex = nextSeparatorIndex;
|
||||
|
||||
result[i] = new FileSystemEntry( rawUri, entryName, isDirectory ? null : FileBrowser.GetExtensionFromFilename( entryName, extractOnlyLastSuffixFromExtensions ), isDirectory );
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
try
|
||||
{
|
||||
FileSystemInfo[] items = new DirectoryInfo( path ).GetFileSystemInfos();
|
||||
FileSystemEntry[] result = new FileSystemEntry[items.Length];
|
||||
int index = 0;
|
||||
for( int i = 0; i < items.Length; i++ )
|
||||
{
|
||||
try
|
||||
{
|
||||
result[index] = new FileSystemEntry( items[i], FileBrowser.GetExtensionFromFilename( items[i].Name, extractOnlyLastSuffixFromExtensions ) );
|
||||
index++;
|
||||
}
|
||||
catch( System.Exception e )
|
||||
{
|
||||
Debug.LogException( e );
|
||||
}
|
||||
}
|
||||
|
||||
if( result.Length != index )
|
||||
System.Array.Resize( ref result, index );
|
||||
|
||||
return result;
|
||||
}
|
||||
catch( System.UnauthorizedAccessException ) { }
|
||||
catch( System.Exception e )
|
||||
{
|
||||
Debug.LogException( e );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string CreateFileInDirectory( string directoryPath, string filename )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( directoryPath ) )
|
||||
return AJC.CallStatic<string>( "CreateSAFEntry", Context, directoryPath, false, filename );
|
||||
#endif
|
||||
|
||||
string path = Path.Combine( directoryPath, filename );
|
||||
using( File.Create( path ) ) { }
|
||||
return path;
|
||||
}
|
||||
|
||||
public static string CreateFolderInDirectory( string directoryPath, string folderName )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( directoryPath ) )
|
||||
return AJC.CallStatic<string>( "CreateSAFEntry", Context, directoryPath, true, folderName );
|
||||
#endif
|
||||
|
||||
string path = Path.Combine( directoryPath, folderName );
|
||||
Directory.CreateDirectory( path );
|
||||
return path;
|
||||
}
|
||||
|
||||
public static void WriteBytesToFile( string targetPath, byte[] bytes )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( targetPath ) )
|
||||
{
|
||||
File.WriteAllBytes( TemporaryFilePath, bytes );
|
||||
AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, TemporaryFilePath, false );
|
||||
File.Delete( TemporaryFilePath );
|
||||
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
File.WriteAllBytes( targetPath, bytes );
|
||||
}
|
||||
|
||||
public static void WriteTextToFile( string targetPath, string text )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( targetPath ) )
|
||||
{
|
||||
File.WriteAllText( TemporaryFilePath, text );
|
||||
AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, TemporaryFilePath, false );
|
||||
File.Delete( TemporaryFilePath );
|
||||
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
File.WriteAllText( targetPath, text );
|
||||
}
|
||||
|
||||
public static void AppendBytesToFile( string targetPath, byte[] bytes )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( targetPath ) )
|
||||
{
|
||||
File.WriteAllBytes( TemporaryFilePath, bytes );
|
||||
AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, TemporaryFilePath, true );
|
||||
File.Delete( TemporaryFilePath );
|
||||
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
using( var stream = new FileStream( targetPath, FileMode.Append, FileAccess.Write ) )
|
||||
{
|
||||
stream.Write( bytes, 0, bytes.Length );
|
||||
}
|
||||
}
|
||||
|
||||
public static void AppendTextToFile( string targetPath, string text )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( targetPath ) )
|
||||
{
|
||||
File.WriteAllText( TemporaryFilePath, text );
|
||||
AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, TemporaryFilePath, true );
|
||||
File.Delete( TemporaryFilePath );
|
||||
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
File.AppendAllText( targetPath, text );
|
||||
}
|
||||
|
||||
private static void AppendFileToFile( string targetPath, string sourceFileToAppend )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( targetPath ) )
|
||||
{
|
||||
AJC.CallStatic( "WriteToSAFEntry", Context, targetPath, sourceFileToAppend, true );
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
using( Stream input = File.OpenRead( sourceFileToAppend ) )
|
||||
using( Stream output = new FileStream( targetPath, FileMode.Append, FileAccess.Write ) )
|
||||
{
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while( ( bytesRead = input.Read( buffer, 0, buffer.Length ) ) > 0 )
|
||||
output.Write( buffer, 0, bytesRead );
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] ReadBytesFromFile( string sourcePath )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( sourcePath ) )
|
||||
{
|
||||
AJC.CallStatic( "ReadFromSAFEntry", Context, sourcePath, TemporaryFilePath );
|
||||
byte[] result = File.ReadAllBytes( TemporaryFilePath );
|
||||
File.Delete( TemporaryFilePath );
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
return File.ReadAllBytes( sourcePath );
|
||||
}
|
||||
|
||||
public static string ReadTextFromFile( string sourcePath )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( sourcePath ) )
|
||||
{
|
||||
AJC.CallStatic( "ReadFromSAFEntry", Context, sourcePath, TemporaryFilePath );
|
||||
string result = File.ReadAllText( TemporaryFilePath );
|
||||
File.Delete( TemporaryFilePath );
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
return File.ReadAllText( sourcePath );
|
||||
}
|
||||
|
||||
public static void CopyFile( string sourcePath, string destinationPath )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAF ) // No need to use ShouldUseSAFForPath because both SAF paths and raw file paths are handled on the native-side
|
||||
{
|
||||
AJC.CallStatic( "CopyFile", Context, sourcePath, destinationPath, false );
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
File.Copy( sourcePath, destinationPath, true );
|
||||
}
|
||||
|
||||
public static void CopyDirectory( string sourcePath, string destinationPath )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAF ) // No need to use ShouldUseSAFForPath because both SAF paths and raw directory paths are handled on the native-side
|
||||
{
|
||||
AJC.CallStatic( "CopyDirectory", Context, sourcePath, destinationPath, false );
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
CopyDirectoryRecursively( new DirectoryInfo( sourcePath ), destinationPath );
|
||||
}
|
||||
|
||||
private static void CopyDirectoryRecursively( DirectoryInfo sourceDirectory, string destinationPath )
|
||||
{
|
||||
Directory.CreateDirectory( destinationPath );
|
||||
|
||||
FileInfo[] files = sourceDirectory.GetFiles();
|
||||
for( int i = 0; i < files.Length; i++ )
|
||||
files[i].CopyTo( Path.Combine( destinationPath, files[i].Name ), true );
|
||||
|
||||
DirectoryInfo[] subDirectories = sourceDirectory.GetDirectories();
|
||||
for( int i = 0; i < subDirectories.Length; i++ )
|
||||
CopyDirectoryRecursively( subDirectories[i], Path.Combine( destinationPath, subDirectories[i].Name ) );
|
||||
}
|
||||
|
||||
public static void MoveFile( string sourcePath, string destinationPath )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAF ) // No need to use ShouldUseSAFForPath because both SAF paths and raw file paths are handled on the native-side
|
||||
{
|
||||
AJC.CallStatic( "CopyFile", Context, sourcePath, destinationPath, true );
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
File.Move( sourcePath, destinationPath );
|
||||
}
|
||||
|
||||
public static void MoveDirectory( string sourcePath, string destinationPath )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAF ) // No need to use ShouldUseSAFForPath because both SAF paths and raw directory paths are handled on the native-side
|
||||
{
|
||||
AJC.CallStatic( "CopyDirectory", Context, sourcePath, destinationPath, true );
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
Directory.Move( sourcePath, destinationPath );
|
||||
}
|
||||
|
||||
public static string RenameFile( string path, string newName )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( path ) )
|
||||
return AJC.CallStatic<string>( "RenameSAFEntry", Context, path, newName );
|
||||
#endif
|
||||
string newPath = Path.Combine( Path.GetDirectoryName( path ), newName );
|
||||
File.Move( path, newPath );
|
||||
|
||||
return newPath;
|
||||
}
|
||||
|
||||
public static string RenameDirectory( string path, string newName )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( path ) )
|
||||
return AJC.CallStatic<string>( "RenameSAFEntry", Context, path, newName );
|
||||
#endif
|
||||
string newPath = Path.Combine( new DirectoryInfo( path ).Parent.FullName, newName );
|
||||
Directory.Move( path, newPath );
|
||||
|
||||
return newPath;
|
||||
}
|
||||
|
||||
public static void DeleteFile( string path )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( path ) )
|
||||
{
|
||||
AJC.CallStatic<bool>( "DeleteSAFEntry", Context, path );
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
File.Delete( path );
|
||||
}
|
||||
|
||||
public static void DeleteDirectory( string path )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( path ) )
|
||||
{
|
||||
AJC.CallStatic<bool>( "DeleteSAFEntry", Context, path );
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
Directory.Delete( path, true );
|
||||
}
|
||||
|
||||
public static string GetFilename( string path )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( path ) )
|
||||
return AJC.CallStatic<string>( "SAFEntryName", Context, path );
|
||||
#endif
|
||||
return Path.GetFileName( path );
|
||||
}
|
||||
|
||||
public static long GetFilesize( string path )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
if( ShouldUseSAFForPath( path ) )
|
||||
return AJC.CallStatic<long>( "SAFEntrySize", Context, path );
|
||||
#endif
|
||||
return new FileInfo( path ).Length;
|
||||
}
|
||||
|
||||
public static System.DateTime GetLastModifiedDate( string path )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
// Credit: https://stackoverflow.com/a/28504416/2373034
|
||||
if( ShouldUseSAFForPath( path ) )
|
||||
return new System.DateTime( 1970, 1, 1, 0, 0, 0 ).AddMilliseconds( AJC.CallStatic<long>( "SAFEntryLastModified", Context, path ) );
|
||||
#endif
|
||||
return new FileInfo( path ).LastWriteTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user