mirror of
https://github.com/crskycode/GARbro.git
synced 2026-07-08 01:30:18 +08:00
reorganized project directory structure.
This commit is contained in:
367
ArcFormats/YuRis/ArcYPF.cs
Normal file
367
ArcFormats/YuRis/ArcYPF.cs
Normal file
@@ -0,0 +1,367 @@
|
||||
//! \file ArcYPF.cs
|
||||
//! \date Mon Jul 14 14:40:06 2014
|
||||
//! \brief YPF resource format implementation.
|
||||
//
|
||||
// Copyright (C) 2014 by morkt
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using GameRes.Compression;
|
||||
using GameRes.Formats.Strings;
|
||||
using GameRes.Formats.Properties;
|
||||
using GameRes.Utility;
|
||||
|
||||
namespace GameRes.Formats.YuRis
|
||||
{
|
||||
public class YpfOptions : ResourceOptions
|
||||
{
|
||||
public uint Key { get; set; }
|
||||
public uint Version { get; set; }
|
||||
}
|
||||
|
||||
[Export(typeof(ArchiveFormat))]
|
||||
public class YpfOpener : ArchiveFormat
|
||||
{
|
||||
public override string Tag { get { return "YPF"; } }
|
||||
public override string Description { get { return arcStrings.YPFDescription; } }
|
||||
public override uint Signature { get { return 0x00465059; } }
|
||||
public override bool IsHierarchic { get { return true; } }
|
||||
public override bool CanCreate { get { return true; } }
|
||||
|
||||
private const uint DefaultKey = 0xffffffff;
|
||||
|
||||
public override ArcFile TryOpen (ArcView file)
|
||||
{
|
||||
uint version = file.View.ReadUInt32 (4);
|
||||
uint count = file.View.ReadUInt32 (8);
|
||||
uint dir_size = file.View.ReadUInt32 (12);
|
||||
if (dir_size < count * 0x17 || count > 0xfffff)
|
||||
return null;
|
||||
if (dir_size > file.View.Reserve (0x20, dir_size))
|
||||
return null;
|
||||
var parser = new Parser (file, version, count, dir_size);
|
||||
|
||||
uint key = QueryEncryptionKey();
|
||||
var dir = parser.ScanDir (key);
|
||||
if (0 == dir.Count)
|
||||
return null;
|
||||
|
||||
return new ArcFile (file, this, dir);
|
||||
}
|
||||
|
||||
public override Stream OpenEntry (ArcFile arc, Entry entry)
|
||||
{
|
||||
var input = arc.File.CreateStream (entry.Offset, entry.Size);
|
||||
var packed_entry = entry as PackedEntry;
|
||||
if (null == packed_entry || !packed_entry.IsPacked)
|
||||
return input;
|
||||
else
|
||||
return new ZLibStream (input, CompressionMode.Decompress);
|
||||
}
|
||||
|
||||
public override ResourceOptions GetDefaultOptions ()
|
||||
{
|
||||
return new YpfOptions
|
||||
{
|
||||
Key = Settings.Default.YPFKey,
|
||||
Version = Settings.Default.YPFVersion,
|
||||
};
|
||||
}
|
||||
|
||||
public override object GetAccessWidget ()
|
||||
{
|
||||
return new GUI.WidgetYPF();
|
||||
}
|
||||
|
||||
public override object GetCreationWidget ()
|
||||
{
|
||||
return new GUI.CreateYPFWidget();
|
||||
}
|
||||
|
||||
uint QueryEncryptionKey ()
|
||||
{
|
||||
var options = Query<YpfOptions> (arcStrings.YPFNotice);
|
||||
return options.Key;
|
||||
}
|
||||
|
||||
internal class YpfEntry : PackedEntry
|
||||
{
|
||||
public byte[] IndexName;
|
||||
public uint NameHash;
|
||||
public byte FileType;
|
||||
public uint CheckSum;
|
||||
}
|
||||
|
||||
delegate uint ChecksumFunc (byte[] data);
|
||||
|
||||
public override void Create (Stream output, IEnumerable<Entry> list, ResourceOptions options,
|
||||
EntryCallback callback)
|
||||
{
|
||||
var ypf_options = GetOptions<YpfOptions> (options);
|
||||
if (null == ypf_options)
|
||||
throw new ArgumentException ("Invalid archive creation options", "options");
|
||||
if (ypf_options.Key > 0xff)
|
||||
throw new InvalidEncryptionScheme (arcStrings.MsgCreationKeyRequired);
|
||||
if (0 == ypf_options.Version)
|
||||
throw new InvalidFormatException (arcStrings.MsgInvalidVersion);
|
||||
|
||||
int callback_count = 0;
|
||||
var encoding = Encodings.cp932.WithFatalFallback();
|
||||
|
||||
ChecksumFunc Checksum = data => Crc32.Compute (data, 0, data.Length);
|
||||
|
||||
uint data_offset = 0x20;
|
||||
var file_table = new List<YpfEntry>();
|
||||
foreach (var entry in list)
|
||||
{
|
||||
try
|
||||
{
|
||||
string file_name = entry.Name;
|
||||
byte[] name_buf = encoding.GetBytes (file_name);
|
||||
if (name_buf.Length > 0xff)
|
||||
throw new InvalidFileName (entry.Name, arcStrings.MsgFileNameTooLong);
|
||||
uint hash = Checksum (name_buf);
|
||||
byte file_type = GetFileType (ypf_options.Version, file_name);
|
||||
|
||||
for (int i = 0; i < name_buf.Length; ++i)
|
||||
name_buf[i] = (byte)(name_buf[i] ^ ypf_options.Key);
|
||||
|
||||
file_table.Add (new YpfEntry {
|
||||
Name = file_name,
|
||||
IndexName = name_buf,
|
||||
NameHash = hash,
|
||||
FileType = file_type,
|
||||
IsPacked = 0 == file_type,
|
||||
});
|
||||
data_offset += (uint)(0x17 + name_buf.Length);
|
||||
}
|
||||
catch (EncoderFallbackException X)
|
||||
{
|
||||
throw new InvalidFileName (entry.Name, arcStrings.MsgIllegalCharacters, X);
|
||||
}
|
||||
}
|
||||
file_table.Sort ((a, b) => a.NameHash.CompareTo (b.NameHash));
|
||||
|
||||
output.Position = data_offset;
|
||||
uint current_offset = data_offset;
|
||||
foreach (var entry in file_table)
|
||||
{
|
||||
if (null != callback)
|
||||
callback (callback_count++, entry, arcStrings.MsgAddingFile);
|
||||
|
||||
entry.Offset = current_offset;
|
||||
using (var input = File.OpenRead (entry.Name))
|
||||
{
|
||||
var file_size = input.Length;
|
||||
if (file_size > uint.MaxValue || current_offset + file_size > uint.MaxValue)
|
||||
throw new FileSizeException();
|
||||
entry.UnpackedSize = (uint)file_size;
|
||||
using (var checked_stream = new CheckedStream (output, new Adler32()))
|
||||
{
|
||||
if (entry.IsPacked)
|
||||
{
|
||||
var start = output.Position;
|
||||
using (var zstream = new ZLibStream (checked_stream, CompressionMode.Compress,
|
||||
CompressionLevel.Level9, true))
|
||||
{
|
||||
input.CopyTo (zstream);
|
||||
}
|
||||
entry.Size = (uint)(output.Position - start);
|
||||
}
|
||||
else
|
||||
{
|
||||
input.CopyTo (checked_stream);
|
||||
entry.Size = entry.UnpackedSize;
|
||||
}
|
||||
checked_stream.Flush();
|
||||
entry.CheckSum = checked_stream.CheckSumValue;
|
||||
current_offset += entry.Size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null != callback)
|
||||
callback (callback_count++, null, arcStrings.MsgWritingIndex);
|
||||
|
||||
output.Position = 0;
|
||||
using (var writer = new BinaryWriter (output, encoding, true))
|
||||
{
|
||||
writer.Write (Signature);
|
||||
writer.Write (ypf_options.Version);
|
||||
writer.Write (file_table.Count);
|
||||
writer.Write (data_offset);
|
||||
writer.BaseStream.Seek (0x20, SeekOrigin.Begin);
|
||||
foreach (var entry in file_table)
|
||||
{
|
||||
writer.Write (entry.NameHash);
|
||||
byte name_len = (byte)~Parser.Decrypt (ypf_options.Version, (byte)entry.IndexName.Length);
|
||||
writer.Write (name_len);
|
||||
writer.Write (entry.IndexName);
|
||||
writer.Write (entry.FileType);
|
||||
writer.Write (entry.IsPacked);
|
||||
writer.Write (entry.UnpackedSize);
|
||||
writer.Write (entry.Size);
|
||||
writer.Write ((uint)entry.Offset);
|
||||
writer.Write (entry.CheckSum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static byte GetFileType (uint version, string name)
|
||||
{
|
||||
// 0x0F7: 0-ybn, 1-bmp, 2-png, 3-jpg, 4-gif, 5-avi, 6-wav, 7-ogg, 8-psd
|
||||
// 0x122, 0x12C, 0x196: 0-ybn, 1-bmp, 2-png, 3-jpg, 4-gif, 5-wav, 6-ogg, 7-psd
|
||||
string ext = Path.GetExtension (name).TrimStart ('.').ToLower();
|
||||
if ("ybn" == ext) return 0;
|
||||
if ("bmp" == ext) return 1;
|
||||
if ("png" == ext) return 2;
|
||||
if ("jpg" == ext || "jpeg" == ext) return 3;
|
||||
if ("gif" == ext) return 4;
|
||||
if ("avi" == ext && 0xf7 == version) return 5;
|
||||
byte type = 0;
|
||||
if ("wav" == ext) type = 5;
|
||||
else if ("ogg" == ext) type = 6;
|
||||
else if ("psd" == ext) type = 7;
|
||||
if (0xf7 == version && 0 != type)
|
||||
++type;
|
||||
return type;
|
||||
}
|
||||
|
||||
private class Parser
|
||||
{
|
||||
ArcView m_file;
|
||||
uint m_version;
|
||||
uint m_count;
|
||||
uint m_dir_size;
|
||||
|
||||
public Parser (ArcView file, uint version, uint count, uint dir_size)
|
||||
{
|
||||
m_file = file;
|
||||
m_count = count;
|
||||
m_dir_size = dir_size;
|
||||
m_version = version;
|
||||
}
|
||||
// 4-name_checksum, 1-name_count, *-name, 1-file_type
|
||||
// 1-pack_flag, 4-size, 4-packed_size, 4-offset, 4-packed_adler32
|
||||
|
||||
public List<Entry> ScanDir (uint key)
|
||||
{
|
||||
uint dir_offset = 0x20;
|
||||
uint dir_remaining = m_dir_size;
|
||||
var dir = new List<Entry> ((int)m_count);
|
||||
for (uint num = 0; num < m_count; ++num)
|
||||
{
|
||||
if (dir_remaining < 0x17)
|
||||
break;
|
||||
dir_remaining -= 0x17;
|
||||
|
||||
uint name_size = Decrypt (m_version, (byte)(m_file.View.ReadByte (dir_offset+4) ^ 0xff));
|
||||
if (name_size > dir_remaining)
|
||||
break;
|
||||
dir_remaining -= name_size;
|
||||
dir_offset += 5;
|
||||
if (0 == name_size)
|
||||
break;
|
||||
if (0xffffffff == key)
|
||||
{
|
||||
if (name_size < 4)
|
||||
break;
|
||||
// assume filename contains '.' and 3-characters extension.
|
||||
key = (uint)(m_file.View.ReadByte (dir_offset+name_size-4) ^ 0x2e);
|
||||
}
|
||||
byte[] raw_name = new byte[name_size];
|
||||
for (int i = 0; i < name_size; ++i)
|
||||
{
|
||||
raw_name[i] = (byte)(m_file.View.ReadByte (dir_offset) ^ key);
|
||||
++dir_offset;
|
||||
}
|
||||
string name = Encodings.cp932.GetString (raw_name);
|
||||
// 0x0F7: 0-ybn, 1-bmp, 2-png, 3-jpg, 4-gif, 5-avi, 6-wav, 7-ogg, 8-psd
|
||||
// 0x122, 0x12C, 0x196: 0-ybn, 1-bmp, 2-png, 3-jpg, 4-gif, 5-wav, 6-ogg, 7-psd
|
||||
int type_id = m_file.View.ReadByte (dir_offset);
|
||||
string type = "";
|
||||
switch (type_id)
|
||||
{
|
||||
case 0:
|
||||
type = "script";
|
||||
break;
|
||||
case 1: case 2: case 3: case 4:
|
||||
type = "image";
|
||||
break;
|
||||
case 5:
|
||||
type = 0xf7 == m_version ? "video" : "audio";
|
||||
break;
|
||||
case 6:
|
||||
case 7:
|
||||
type = "audio";
|
||||
break;
|
||||
}
|
||||
var entry = new PackedEntry { Name = name, Type = type };
|
||||
entry.IsPacked = 1 == m_file.View.ReadByte (dir_offset+1);
|
||||
entry.UnpackedSize = m_file.View.ReadUInt32 (dir_offset+2);
|
||||
entry.Size = m_file.View.ReadUInt32 (dir_offset+6);
|
||||
entry.Offset = m_file.View.ReadUInt32 (dir_offset+10);
|
||||
if (entry.CheckPlacement (m_file.MaxOffset))
|
||||
dir.Add (entry);
|
||||
dir_offset += 0x12;
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
static readonly byte[] s_crypt_table = {
|
||||
0x03,0x48,0x06,0x35, // 0x122, 0x196
|
||||
0x0C,0x10,0x11,0x19,0x1C,0x1E, // 0x0F7
|
||||
0x09,0x0B,0x0D,0x13,0x15,0x1B, // 0x12C
|
||||
0x20,0x23,0x26,0x29,
|
||||
0x2C,0x2F,0x2E,0x32,
|
||||
};
|
||||
// 0xFF 0x0F7 "Four-Leaf" adler32
|
||||
// 0x34 0x122 "Neko Koi!" crc32
|
||||
// 0x28 0x12C "Suzukaze no Melt" (no recovery - 00 00 00 00)
|
||||
// 0xFF 0x196 "Mamono Musume-tachi to no Rakuen ~Slime & Scylla~"
|
||||
|
||||
static public byte Decrypt (uint version, byte value)
|
||||
{
|
||||
int pos = 4;
|
||||
if (version >= 0x100)
|
||||
{
|
||||
if (version >= 0x12c && version < 0x196)
|
||||
pos = 10;
|
||||
else
|
||||
pos = 0;
|
||||
}
|
||||
pos = Array.IndexOf (s_crypt_table, value, pos);
|
||||
if (-1 == pos)
|
||||
return value;
|
||||
if (0 != (pos & 1))
|
||||
return s_crypt_table[pos-1];
|
||||
else
|
||||
return s_crypt_table[pos+1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
ArcFormats/YuRis/CreateYPFWidget.xaml
Normal file
27
ArcFormats/YuRis/CreateYPFWidget.xaml
Normal file
@@ -0,0 +1,27 @@
|
||||
<Grid x:Class="GameRes.Formats.GUI.CreateYPFWidget"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:p="clr-namespace:GameRes.Formats.Properties"
|
||||
xmlns:s="clr-namespace:GameRes.Formats.Strings"
|
||||
xmlns:gui="clr-namespace:GameRes.Formats.GUI">
|
||||
<Grid.Resources>
|
||||
<gui:KeyToStringConverter x:Key="guiKeyConverter" />
|
||||
<gui:VersionToStringConverter x:Key="guiVersionConverter" />
|
||||
</Grid.Resources>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition MinWidth="50" Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Label Content="{x:Static s:arcStrings.YPFLabelVersion}" ToolTip="{x:Static s:arcStrings.TooltipHex}"
|
||||
Target="{Binding ElementName=Version}" Grid.Column="0" Grid.Row="0" Padding="0,1" VerticalAlignment="Center" HorizontalAlignment="Right" HorizontalContentAlignment="Right"/>
|
||||
<TextBox Name="Version" Text="{Binding Source={x:Static p:Settings.Default}, Path=YPFVersion, Mode=TwoWay, Converter={StaticResource guiVersionConverter}}"
|
||||
ToolTip="{x:Static s:arcStrings.TooltipHex}" Margin="5" Width="50" Grid.Column="1" Grid.Row="0"/>
|
||||
<Label Content="{x:Static s:arcStrings.YPFLabelKey}" ToolTip="{x:Static s:arcStrings.TooltipHex}"
|
||||
Target="{Binding ElementName=Passkey}" Grid.Column="0" Grid.Row="1" Padding="0,1" VerticalAlignment="Center" HorizontalAlignment="Right" HorizontalContentAlignment="Right"/>
|
||||
<TextBox Name="Passkey" Text="{Binding Source={x:Static p:Settings.Default}, Path=YPFKey, Mode=TwoWay, Converter={StaticResource guiKeyConverter}}"
|
||||
ToolTip="{x:Static s:arcStrings.TooltipHex}" Margin="5" Width="50" Grid.Column="1" Grid.Row="1"/>
|
||||
</Grid>
|
||||
62
ArcFormats/YuRis/CreateYPFWidget.xaml.cs
Normal file
62
ArcFormats/YuRis/CreateYPFWidget.xaml.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace GameRes.Formats.GUI
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for CreateYPFWidget.xaml
|
||||
/// </summary>
|
||||
public partial class CreateYPFWidget : Grid
|
||||
{
|
||||
public CreateYPFWidget ()
|
||||
{
|
||||
InitializeComponent ();
|
||||
}
|
||||
}
|
||||
|
||||
[ValueConversion (typeof (uint), typeof (string))]
|
||||
class VersionToStringConverter : IValueConverter
|
||||
{
|
||||
public object Convert (object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
uint version = (uint)value;
|
||||
return version.ToString ("X");
|
||||
}
|
||||
|
||||
public object ConvertBack (object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
string s = value as string;
|
||||
if (string.IsNullOrEmpty (s))
|
||||
return null;
|
||||
uint version;
|
||||
if (!uint.TryParse (s, NumberStyles.HexNumber, culture, out version))
|
||||
return null;
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
[ValueConversion (typeof (uint), typeof (string))]
|
||||
class KeyToStringConverter : IValueConverter
|
||||
{
|
||||
public object Convert (object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
uint key = (uint)value;
|
||||
if (key > 0xff)
|
||||
return "";
|
||||
return key.ToString ("X2");
|
||||
}
|
||||
|
||||
public object ConvertBack (object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
string s = value as string;
|
||||
if (string.IsNullOrEmpty (s))
|
||||
return uint.MaxValue;
|
||||
uint key;
|
||||
if (!uint.TryParse (s, NumberStyles.HexNumber, culture, out key) || key > 0xff)
|
||||
return uint.MaxValue;
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
ArcFormats/YuRis/WidgetYPF.xaml
Normal file
18
ArcFormats/YuRis/WidgetYPF.xaml
Normal file
@@ -0,0 +1,18 @@
|
||||
<Grid x:Class="GameRes.Formats.GUI.WidgetYPF"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:s="clr-namespace:GameRes.Formats.Strings"
|
||||
xmlns:p="clr-namespace:GameRes.Formats.Properties"
|
||||
xmlns:gui="clr-namespace:GameRes.Formats.GUI"
|
||||
MaxWidth="250">
|
||||
<Grid.Resources>
|
||||
<gui:KeyToStringConverter x:Key="guiKeyConverter" />
|
||||
</Grid.Resources>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition MinWidth="50" Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{x:Static s:arcStrings.YPFLabelKey}" Target="{Binding ElementName=Passkey}" ToolTip="{x:Static s:arcStrings.TooltipHex}" Grid.Column="0" Grid.Row="0" Padding="0,1" VerticalAlignment="Center" HorizontalAlignment="Right"/>
|
||||
<TextBox Name="Passkey" Text="{Binding Source={x:Static p:Settings.Default}, Path=YPFKey, Mode=TwoWay, Converter={StaticResource guiKeyConverter}}"
|
||||
ToolTip="{x:Static s:arcStrings.TooltipHex}" Margin="5" Width="50" Grid.Column="1" Grid.Row="0"/>
|
||||
</Grid>
|
||||
26
ArcFormats/YuRis/WidgetYPF.xaml.cs
Normal file
26
ArcFormats/YuRis/WidgetYPF.xaml.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using GameRes.Formats.Properties;
|
||||
|
||||
namespace GameRes.Formats.GUI
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for WidgetYPF.xaml
|
||||
/// </summary>
|
||||
public partial class WidgetYPF : Grid
|
||||
{
|
||||
public WidgetYPF ()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public uint? GetKey ()
|
||||
{
|
||||
uint key;
|
||||
if (uint.TryParse (this.Passkey.Text, out key) && key < 0x100)
|
||||
return key;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user