2019-05-02 00:08:09 +02:00
|
|
|
|
using Syroot.BinaryData;
|
2019-06-11 21:39:23 +02:00
|
|
|
|
using System;
|
2019-05-02 00:08:09 +02:00
|
|
|
|
using System.IO;
|
|
|
|
|
using System.IO.Compression;
|
|
|
|
|
using OpenTK;
|
|
|
|
|
using System.Windows.Forms;
|
2019-07-16 23:35:21 +02:00
|
|
|
|
using Toolbox.Library.Forms;
|
2019-05-02 00:08:09 +02:00
|
|
|
|
|
2019-07-16 23:35:21 +02:00
|
|
|
|
namespace Toolbox.Library.IO
|
2019-05-02 00:08:09 +02:00
|
|
|
|
{
|
|
|
|
|
public class STFileSaver
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Saves the <see cref="IFileFormat"/> as a file from the given <param name="FileName">
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="IFileFormat">The format instance of the file being saved</param>
|
|
|
|
|
/// <param name="FileName">The name of the file</param>
|
|
|
|
|
/// <param name="Alignment">The Alignment used for compression. Used for Yaz0 compression type. </param>
|
|
|
|
|
/// <param name="EnableDialog">Toggle for showing compression dialog</param>
|
|
|
|
|
/// <returns></returns>
|
2019-06-11 21:39:23 +02:00
|
|
|
|
public static void SaveFileFormat(IFileFormat FileFormat, string FileName, bool EnableDialog = true, string DetailsLog = "")
|
2019-05-02 00:08:09 +02:00
|
|
|
|
{
|
2019-05-09 20:49:11 +02:00
|
|
|
|
//These always get created on loading a file,however not on creating a new file
|
|
|
|
|
if (FileFormat.IFileInfo == null)
|
2019-06-12 02:59:53 +02:00
|
|
|
|
throw new System.NotImplementedException("Make sure to impliment a IFileInfo instance if a format is being created!");
|
2019-05-09 20:49:11 +02:00
|
|
|
|
|
2019-05-02 00:08:09 +02:00
|
|
|
|
Cursor.Current = Cursors.WaitCursor;
|
|
|
|
|
FileFormat.FilePath = FileName;
|
|
|
|
|
|
2019-12-08 02:16:13 +01:00
|
|
|
|
string compressionLog = "";
|
|
|
|
|
if (FileFormat.IFileInfo.FileIsCompressed || FileFormat.IFileInfo.InArchive
|
|
|
|
|
|| Path.GetExtension(FileName) == ".szs" || Path.GetExtension(FileName) == ".sbfres")
|
2019-08-06 23:35:18 +02:00
|
|
|
|
{
|
|
|
|
|
//Todo find more optmial way to handle memory with files in archives
|
|
|
|
|
//Also make compression require streams
|
|
|
|
|
var mem = new System.IO.MemoryStream();
|
|
|
|
|
FileFormat.Save(mem);
|
2019-09-16 01:13:01 +02:00
|
|
|
|
mem = new System.IO.MemoryStream(mem.ToArray());
|
2019-05-02 00:08:09 +02:00
|
|
|
|
|
2019-09-16 01:13:01 +02:00
|
|
|
|
FileFormat.IFileInfo.DecompressedSize = (uint)mem.Length;
|
2019-05-02 00:08:09 +02:00
|
|
|
|
|
2019-09-16 01:13:01 +02:00
|
|
|
|
var finalStream = CompressFileFormat(
|
|
|
|
|
FileFormat.IFileInfo.FileCompression,
|
|
|
|
|
mem,
|
2019-08-06 23:35:18 +02:00
|
|
|
|
FileFormat.IFileInfo.FileIsCompressed,
|
|
|
|
|
FileFormat.IFileInfo.Alignment,
|
|
|
|
|
FileName,
|
|
|
|
|
EnableDialog);
|
2019-05-02 00:08:09 +02:00
|
|
|
|
|
2019-12-08 02:16:13 +01:00
|
|
|
|
compressionLog = finalStream.Item2;
|
|
|
|
|
Stream compressionStream = finalStream.Item1;
|
2019-06-12 23:28:06 +02:00
|
|
|
|
|
2019-12-08 02:16:13 +01:00
|
|
|
|
FileFormat.IFileInfo.CompressedSize = (uint)compressionStream.Length;
|
|
|
|
|
compressionStream.ExportToFile(FileName);
|
|
|
|
|
|
|
|
|
|
DetailsLog += "\n" + SatisfyFileTables(FileFormat, FileName, compressionStream,
|
2019-08-06 23:35:18 +02:00
|
|
|
|
FileFormat.IFileInfo.DecompressedSize,
|
|
|
|
|
FileFormat.IFileInfo.CompressedSize,
|
|
|
|
|
FileFormat.IFileInfo.FileIsCompressed);
|
2019-09-16 01:13:01 +02:00
|
|
|
|
|
2019-12-08 02:16:13 +01:00
|
|
|
|
compressionStream.Flush();
|
|
|
|
|
compressionStream.Close();
|
2019-08-06 23:35:18 +02:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
//Check if a stream is active and the file is beinng saved to the same opened file
|
|
|
|
|
if (FileFormat is ISaveOpenedFileStream && FileFormat.FilePath == FileName && File.Exists(FileName))
|
|
|
|
|
{
|
|
|
|
|
string savedPath = Path.GetDirectoryName(FileName);
|
|
|
|
|
string tempPath = Path.Combine(savedPath, "tempST.bin");
|
|
|
|
|
|
|
|
|
|
//Save a temporary file first to not disturb the opened file
|
|
|
|
|
using (var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite))
|
|
|
|
|
{
|
|
|
|
|
FileFormat.Save(fileStream);
|
|
|
|
|
|
|
|
|
|
//After saving is done remove the existing file
|
|
|
|
|
File.Delete(FileName);
|
|
|
|
|
|
|
|
|
|
//Now move and rename our temp file to the new file path
|
|
|
|
|
File.Move(tempPath, FileName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
using (var fileStream = new FileStream(FileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite))
|
|
|
|
|
{
|
|
|
|
|
FileFormat.Save(fileStream);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-11 21:39:23 +02:00
|
|
|
|
|
2019-12-08 02:16:13 +01:00
|
|
|
|
if (compressionLog != string.Empty)
|
|
|
|
|
MessageBox.Show($"File has been saved to {FileName}. Compressed time: {compressionLog}", "Save Notification");
|
|
|
|
|
else
|
|
|
|
|
MessageBox.Show($"File has been saved to {FileName}", "Save Notification");
|
2019-06-11 21:43:45 +02:00
|
|
|
|
|
|
|
|
|
// STSaveLogDialog.Show($"File has been saved to {FileName}", "Save Notification", DetailsLog);
|
2019-05-02 00:08:09 +02:00
|
|
|
|
Cursor.Current = Cursors.Default;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-16 01:13:01 +02:00
|
|
|
|
private static string SatisfyFileTables(IFileFormat FileFormat, string FilePath, Stream Data, uint DecompressedSize, uint CompressedSize, bool IsYaz0Compressed)
|
2019-06-11 21:39:23 +02:00
|
|
|
|
{
|
|
|
|
|
string FileLog = "";
|
|
|
|
|
|
|
|
|
|
bool IsBotwFile = FilePath.IsSubPathOf(Runtime.BotwGamePath);
|
|
|
|
|
bool IsTPHDFile = FilePath.IsSubPathOf(Runtime.TpGamePath);
|
|
|
|
|
|
|
|
|
|
if (Runtime.ResourceTables.BotwTable && IsBotwFile)
|
|
|
|
|
{
|
|
|
|
|
string newFilePath = FilePath.Replace(Runtime.BotwGamePath, string.Empty).Remove(0, 1);
|
|
|
|
|
newFilePath = newFilePath.Replace(".s", ".");
|
2019-06-12 00:05:07 +02:00
|
|
|
|
newFilePath = newFilePath.Replace( @"\", "/");
|
|
|
|
|
|
2019-06-11 21:39:23 +02:00
|
|
|
|
string RealExtension = Path.GetExtension(newFilePath).Replace(".s", ".");
|
|
|
|
|
|
|
|
|
|
string RstbPath = Path.Combine($"{Runtime.BotwGamePath}",
|
|
|
|
|
"System", "Resource", "ResourceSizeTable.product.srsizetable");
|
|
|
|
|
|
|
|
|
|
RSTB BotwResourceTable = new RSTB();
|
|
|
|
|
BotwResourceTable.LoadFile(RstbPath);
|
|
|
|
|
|
|
|
|
|
//Create a backup first if one doesn't exist
|
|
|
|
|
if (!File.Exists($"{RstbPath}.backup"))
|
|
|
|
|
{
|
2019-06-12 00:05:07 +02:00
|
|
|
|
STConsole.WriteLine($"RSTB File found. Creating backup...");
|
|
|
|
|
|
2019-06-11 21:39:23 +02:00
|
|
|
|
BotwResourceTable.Write(new FileWriter($"{RstbPath}.backup"));
|
|
|
|
|
File.WriteAllBytes($"{RstbPath}.backup", EveryFileExplorer.YAZ0.Compress($"{RstbPath}.backup"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Now apply the file table then save the table
|
|
|
|
|
if (BotwResourceTable.IsInTable(newFilePath))
|
2019-06-12 01:35:02 +02:00
|
|
|
|
{
|
2019-06-11 21:39:23 +02:00
|
|
|
|
FileLog += $"File found in resource table! {newFilePath}";
|
2019-06-12 01:35:02 +02:00
|
|
|
|
STConsole.WriteLine(FileLog, 1);
|
|
|
|
|
}
|
2019-06-11 21:39:23 +02:00
|
|
|
|
else
|
2019-06-12 01:35:02 +02:00
|
|
|
|
{
|
2019-06-11 21:39:23 +02:00
|
|
|
|
FileLog += $"File NOT found in resource table! {newFilePath}";
|
2019-06-12 01:35:02 +02:00
|
|
|
|
STConsole.WriteLine(FileLog, 0);
|
|
|
|
|
|
|
|
|
|
}
|
2019-06-11 21:39:23 +02:00
|
|
|
|
|
2019-06-15 17:04:29 +02:00
|
|
|
|
BotwResourceTable.SetEntry(newFilePath, Data, IsYaz0Compressed);
|
2019-06-11 21:39:23 +02:00
|
|
|
|
BotwResourceTable.Write(new FileWriter(RstbPath));
|
|
|
|
|
File.WriteAllBytes(RstbPath, EveryFileExplorer.YAZ0.Compress(RstbPath));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Runtime.ResourceTables.TpTable && IsTPHDFile)
|
|
|
|
|
{
|
2019-06-12 01:51:27 +02:00
|
|
|
|
string newFilePath = FilePath.Replace(Runtime.TpGamePath, string.Empty).Remove(0, 1);
|
2019-06-12 01:35:02 +02:00
|
|
|
|
newFilePath = newFilePath.Replace(@"\", "/");
|
|
|
|
|
|
2019-06-12 01:51:27 +02:00
|
|
|
|
//Read the compressed tables and set the new sizes if paths match
|
2019-06-12 01:35:02 +02:00
|
|
|
|
TPFileSizeTable CompressedFileTbl = new TPFileSizeTable();
|
|
|
|
|
CompressedFileTbl.ReadCompressedTable(new FileReader($"{Runtime.TpGamePath}/FileSizeList.txt"));
|
|
|
|
|
if (CompressedFileTbl.IsInFileSizeList(newFilePath))
|
|
|
|
|
{
|
|
|
|
|
STConsole.WriteLine("Found matching path in File Size List table! " + newFilePath, 1);
|
|
|
|
|
CompressedFileTbl.SetFileSizeEntry(newFilePath, CompressedSize);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
STConsole.WriteLine("Failed to find path in File Size List table! " + newFilePath, 0);
|
|
|
|
|
|
2019-06-12 01:51:27 +02:00
|
|
|
|
//Read decompressed file sizes
|
2019-06-12 01:35:02 +02:00
|
|
|
|
TPFileSizeTable DecompressedFileTbl = new TPFileSizeTable();
|
|
|
|
|
DecompressedFileTbl.ReadDecompressedTable(new FileReader($"{Runtime.TpGamePath}/DecompressedSizeList.txt"));
|
|
|
|
|
|
2019-06-12 02:59:53 +02:00
|
|
|
|
newFilePath = $"./DVDRoot/{newFilePath}";
|
|
|
|
|
newFilePath = newFilePath.Replace(".gz", string.Empty);
|
2019-06-12 01:51:27 +02:00
|
|
|
|
|
|
|
|
|
//Write the decompressed file size
|
|
|
|
|
if (DecompressedFileTbl.IsInDecompressedFileSizeList(newFilePath))
|
|
|
|
|
{
|
|
|
|
|
STConsole.WriteLine("Found matching path in File Size List table! " + newFilePath, 1);
|
2019-06-12 03:10:23 +02:00
|
|
|
|
DecompressedFileTbl.SetDecompressedFileSizeEntry(newFilePath, CompressedSize, DecompressedSize);
|
2019-06-12 01:51:27 +02:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
STConsole.WriteLine("Failed to find path in File Size List table! " + newFilePath, 0);
|
|
|
|
|
|
2019-06-15 16:44:22 +02:00
|
|
|
|
if (FileFormat == null)
|
|
|
|
|
return FileLog;
|
|
|
|
|
|
2019-06-12 01:51:27 +02:00
|
|
|
|
//Check if archive type
|
2019-06-12 01:35:02 +02:00
|
|
|
|
bool IsArchive = false;
|
|
|
|
|
foreach (var inter in FileFormat.GetType().GetInterfaces())
|
2019-06-12 02:59:53 +02:00
|
|
|
|
{
|
|
|
|
|
if (inter == typeof(IArchiveFile))
|
2019-06-12 01:35:02 +02:00
|
|
|
|
IsArchive = true;
|
2019-06-12 02:59:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-12 01:35:02 +02:00
|
|
|
|
|
2019-06-12 01:51:27 +02:00
|
|
|
|
//Write all the file sizes in the archive if it's an archive type
|
2019-06-12 03:10:23 +02:00
|
|
|
|
//Note this seems uneeded atm
|
|
|
|
|
//Todo store both compressed and decompressed sizes in archive info
|
2019-06-12 03:20:50 +02:00
|
|
|
|
/* if (IsArchive)
|
|
|
|
|
{
|
|
|
|
|
IArchiveFile Archive = (IArchiveFile)FileFormat;
|
|
|
|
|
foreach (var file in Archive.Files)
|
|
|
|
|
{
|
|
|
|
|
uint DecompressedArchiveFileSize = (uint)file.FileData.Length;
|
|
|
|
|
string ArchiveFilePath = $"/DVDRoot/{file.FileName}";
|
|
|
|
|
|
|
|
|
|
if (DecompressedFileTbl.IsInDecompressedFileSizeList(ArchiveFilePath))
|
|
|
|
|
{
|
|
|
|
|
STConsole.WriteLine("Found matching path in File Size List table! " + ArchiveFilePath, 1);
|
|
|
|
|
DecompressedFileTbl.SetDecompressedFileSizeEntry(ArchiveFilePath, DecompressedArchiveFileSize, DecompressedArchiveFileSize);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
STConsole.WriteLine("Failed to find path in File Size List table! " + ArchiveFilePath, 0);
|
|
|
|
|
}
|
|
|
|
|
}*/
|
2019-06-11 21:39:23 +02:00
|
|
|
|
|
2019-06-12 23:41:03 +02:00
|
|
|
|
CompressedFileTbl.WriteCompressedTable(new FileWriter($"{Runtime.TpGamePath}/FileSizeList.txt"));
|
|
|
|
|
DecompressedFileTbl.WriteDecompressedTable(new FileWriter($"{Runtime.TpGamePath}/DecompressedSizeList.txt"));
|
2019-06-11 21:39:23 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return FileLog;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2019-09-16 01:13:01 +02:00
|
|
|
|
public static void SaveFileFormat(byte[] data, bool FileIsCompressed, ICompressionFormat CompressionFormat,
|
|
|
|
|
int Alignment, string FileName, bool EnableDialog = true, string DetailsLog = "")
|
2019-05-02 00:08:09 +02:00
|
|
|
|
{
|
2019-06-15 16:44:22 +02:00
|
|
|
|
uint DecompressedSize = (uint)data.Length;
|
|
|
|
|
|
2019-05-02 00:08:09 +02:00
|
|
|
|
Cursor.Current = Cursors.WaitCursor;
|
2019-12-08 02:16:13 +01:00
|
|
|
|
var compressedData = CompressFileFormat(CompressionFormat, new MemoryStream(data), FileIsCompressed, Alignment, FileName, EnableDialog);
|
|
|
|
|
string compressionLog = compressedData.Item2;
|
|
|
|
|
Stream FinalData = compressedData.Item1;
|
|
|
|
|
|
2019-09-16 01:13:01 +02:00
|
|
|
|
FinalData.ExportToFile(FileName);
|
2019-06-11 21:39:23 +02:00
|
|
|
|
|
2019-06-15 16:44:22 +02:00
|
|
|
|
uint CompressedSize = (uint)FinalData.Length;
|
|
|
|
|
|
2019-09-16 01:13:01 +02:00
|
|
|
|
DetailsLog += "\n" + SatisfyFileTables(null, FileName, new MemoryStream(data),
|
2019-06-15 16:44:22 +02:00
|
|
|
|
DecompressedSize,
|
|
|
|
|
CompressedSize,
|
|
|
|
|
FileIsCompressed);
|
|
|
|
|
|
2019-09-16 01:13:01 +02:00
|
|
|
|
FinalData.Flush();
|
|
|
|
|
FinalData.Close();
|
|
|
|
|
|
2019-06-11 21:43:45 +02:00
|
|
|
|
MessageBox.Show($"File has been saved to {FileName}", "Save Notification");
|
|
|
|
|
|
|
|
|
|
// STSaveLogDialog.Show($"File has been saved to {FileName}", "Save Notification", DetailsLog);
|
2019-05-02 00:08:09 +02:00
|
|
|
|
Cursor.Current = Cursors.Default;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-26 23:58:33 +02:00
|
|
|
|
public static void BatchFileTable(string directory)
|
|
|
|
|
{
|
|
|
|
|
foreach (var file in Directory.GetDirectories(directory))
|
|
|
|
|
BatchFileTable(file);
|
|
|
|
|
|
|
|
|
|
foreach (var file in Directory.GetFiles(directory))
|
|
|
|
|
{
|
|
|
|
|
var fileStream = File.OpenRead(file);
|
|
|
|
|
uint compLength = (uint)fileStream.Length;
|
|
|
|
|
uint decompLength = (uint)fileStream.Length;
|
|
|
|
|
bool yaz0 = false;
|
|
|
|
|
foreach (ICompressionFormat compressionFormat in FileManager.GetCompressionFormats())
|
|
|
|
|
{
|
|
|
|
|
fileStream.Position = 0;
|
|
|
|
|
if (compressionFormat.Identify(fileStream, file))
|
|
|
|
|
{
|
|
|
|
|
fileStream.Position = 0;
|
|
|
|
|
if (compressionFormat is Yaz0)
|
|
|
|
|
yaz0 = true;
|
|
|
|
|
|
|
|
|
|
var decomp = compressionFormat.Decompress(fileStream);
|
|
|
|
|
decompLength = (uint)decomp.Length;
|
|
|
|
|
decomp.Close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SatisfyFileTables(null, file, File.OpenRead(file), compLength, decompLength, yaz0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-08 02:16:13 +01:00
|
|
|
|
private static Tuple<Stream, string> CompressFileFormat(ICompressionFormat compressionFormat, Stream data, bool FileIsCompressed, int Alignment,
|
2019-09-16 01:13:01 +02:00
|
|
|
|
string FileName, bool EnableDialog = true)
|
2019-05-02 00:08:09 +02:00
|
|
|
|
{
|
|
|
|
|
string extension = Path.GetExtension(FileName);
|
|
|
|
|
|
|
|
|
|
if (extension == ".szs" || extension == ".sbfres")
|
|
|
|
|
{
|
|
|
|
|
FileIsCompressed = true;
|
2019-09-16 01:13:01 +02:00
|
|
|
|
compressionFormat = new Yaz0();
|
2019-05-02 00:08:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-16 01:13:01 +02:00
|
|
|
|
if (compressionFormat == null)
|
2019-12-08 02:16:13 +01:00
|
|
|
|
return Tuple.Create(data, "");
|
2019-09-16 01:13:01 +02:00
|
|
|
|
|
2019-07-06 01:02:18 +02:00
|
|
|
|
bool CompressFile = false;
|
2019-05-02 00:08:09 +02:00
|
|
|
|
if (EnableDialog && FileIsCompressed)
|
|
|
|
|
{
|
2019-07-03 18:56:28 +02:00
|
|
|
|
if (Runtime.AlwaysCompressOnSave)
|
|
|
|
|
CompressFile = true;
|
|
|
|
|
else
|
|
|
|
|
{
|
2019-09-16 01:13:01 +02:00
|
|
|
|
DialogResult save = MessageBox.Show($"Compress file with {compressionFormat}?", "File Save", MessageBoxButtons.YesNo);
|
2019-07-03 18:56:28 +02:00
|
|
|
|
CompressFile = (save == DialogResult.Yes);
|
|
|
|
|
}
|
2019-07-06 01:02:18 +02:00
|
|
|
|
}
|
|
|
|
|
else if (FileIsCompressed)
|
|
|
|
|
CompressFile = true;
|
2019-05-02 00:08:09 +02:00
|
|
|
|
|
2019-09-16 01:13:01 +02:00
|
|
|
|
Console.WriteLine($"FileIsCompressed {FileIsCompressed} CompressFile {CompressFile} CompressionType {compressionFormat}");
|
2019-07-06 01:06:24 +02:00
|
|
|
|
|
2019-07-06 01:02:18 +02:00
|
|
|
|
if (CompressFile)
|
2019-09-17 00:58:09 +02:00
|
|
|
|
{
|
|
|
|
|
if (compressionFormat is Yaz0)
|
|
|
|
|
((Yaz0)compressionFormat).Alignment = Alignment;
|
|
|
|
|
|
2019-12-08 02:16:13 +01:00
|
|
|
|
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
|
|
|
|
|
sw.Start();
|
|
|
|
|
|
|
|
|
|
var comp = compressionFormat.Compress(data);
|
|
|
|
|
sw.Stop();
|
|
|
|
|
TimeSpan ts = sw.Elapsed;
|
|
|
|
|
string message = string.Format("{0:D2}:{1:D2}:{2:D2}", ts.Minutes, ts.Seconds, ts.Milliseconds);
|
|
|
|
|
Console.WriteLine($"Compression Time : {message}");
|
|
|
|
|
return Tuple.Create(comp, message);
|
2019-09-17 00:58:09 +02:00
|
|
|
|
}
|
2019-05-04 03:08:29 +02:00
|
|
|
|
|
2019-12-08 02:16:13 +01:00
|
|
|
|
return Tuple.Create(data, "");
|
2019-05-02 00:08:09 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|