Merge branch 'master' of https://github.com/TASVideos/bizhawk
This commit is contained in:
commit
73bd94e09b
|
@ -366,7 +366,7 @@ namespace BizHawk.Client.DiscoHawk
|
|||
if (!item.Exists)
|
||||
sw.Write("(---missing---)");
|
||||
else
|
||||
sw.Write("({0:X2} - {1})", (byte)item.Control, item.LBATimestamp);
|
||||
sw.Write("({0:X2} - {1})", (byte)item.Control, item.LBA);
|
||||
};
|
||||
|
||||
|
||||
|
@ -399,7 +399,7 @@ namespace BizHawk.Client.DiscoHawk
|
|||
{
|
||||
if (src_toc.TOCItems[t].Exists != dst_toc.TOCItems[t].Exists
|
||||
|| src_toc.TOCItems[t].Control != dst_toc.TOCItems[t].Control
|
||||
|| src_toc.TOCItems[t].LBATimestamp.Sector != dst_toc.TOCItems[t].LBATimestamp.Sector
|
||||
|| src_toc.TOCItems[t].LBA != dst_toc.TOCItems[t].LBA
|
||||
)
|
||||
{
|
||||
sw.WriteLine("Mismatch in TOCItem");
|
||||
|
|
|
@ -422,7 +422,7 @@ namespace BizHawk.Emulation.Cores.Sega.Saturn
|
|||
|
||||
rTOC[99] = (int)(rTOC[0] & 0xff000000 | 0x010000);
|
||||
rTOC[100] = (int)(rTOC[ntrk - 1] & 0xff000000 | (uint)(ntrk << 16));
|
||||
rTOC[101] = (int)(rTOC[ntrk - 1] & 0xff000000 | (uint)(CD.TOC.LeadoutLBA.Sector)); //zero 03-jul-2014 - maybe off by 150
|
||||
rTOC[101] = (int)(rTOC[ntrk - 1] & 0xff000000 | (uint)(CD.TOC.LeadoutLBA)); //zero 03-jul-2014 - maybe off by 150
|
||||
|
||||
|
||||
Marshal.Copy(rTOC, 0, dest, 102);
|
||||
|
|
|
@ -178,14 +178,14 @@ namespace BizHawk.Emulation.Cores.Sony.PSX
|
|||
{
|
||||
var item = Disc.TOC.TOCItems[i];
|
||||
tracks101[i].adr = (byte)(item.Exists ? 1 : 0);
|
||||
tracks101[i].lba = (uint)item.LBATimestamp.Sector;
|
||||
tracks101[i].lba = (uint)item.LBA;
|
||||
tracks101[i].control = (byte)item.Control;
|
||||
}
|
||||
|
||||
////the lead-out track is to be synthesized
|
||||
tracks101[read_target->last_track + 1].adr = 1;
|
||||
tracks101[read_target->last_track + 1].control = 0;
|
||||
tracks101[read_target->last_track + 1].lba = (uint)Disc.TOC.LeadoutLBA.Sector;
|
||||
tracks101[read_target->last_track + 1].lba = (uint)Disc.TOC.LeadoutLBA;
|
||||
|
||||
//element 100 is to be copied as the lead-out track
|
||||
tracks101[100] = tracks101[read_target->last_track + 1];
|
||||
|
|
|
@ -326,7 +326,6 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
public string ImgPath;
|
||||
public string SubPath;
|
||||
public string CcdPath;
|
||||
public int NumImgSectors;
|
||||
}
|
||||
|
||||
public static LoadResults LoadCCDPath(string path)
|
||||
|
@ -337,17 +336,8 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
ret.SubPath = Path.ChangeExtension(path, ".sub");
|
||||
try
|
||||
{
|
||||
if(!File.Exists(path)) throw new CCDParseException("Malformed CCD format: nonexistent CCD file!");
|
||||
if (!File.Exists(ret.ImgPath)) throw new CCDParseException("Malformed CCD format: nonexistent IMG file!");
|
||||
if (!File.Exists(ret.SubPath)) throw new CCDParseException("Malformed CCD format: nonexistent SUB file!");
|
||||
if (!File.Exists(path)) throw new CCDParseException("Malformed CCD format: nonexistent CCD file!");
|
||||
|
||||
//quick check of .img and .sub sizes
|
||||
long imgLen = new FileInfo(ret.ImgPath).Length;
|
||||
long subLen = new FileInfo(ret.SubPath).Length;
|
||||
if(imgLen % 2352 != 0) throw new CCDParseException("Malformed CCD format: IMG file length not multiple of 2352");
|
||||
ret.NumImgSectors = (int)(imgLen / 2352);
|
||||
if (subLen != ret.NumImgSectors * 96) throw new CCDParseException("Malformed CCD format: SUB file length not matching IMG");
|
||||
|
||||
CCDFile ccdf;
|
||||
using (var infCCD = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
ccdf = new CCD_Format().ParseFrom(infCCD);
|
||||
|
@ -403,12 +393,12 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
sw.WriteLine("AMin={0}", entry.QData.min.DecimalValue);
|
||||
sw.WriteLine("ASec={0}", entry.QData.sec.DecimalValue);
|
||||
sw.WriteLine("AFrame={0}", entry.QData.frame.DecimalValue);
|
||||
sw.WriteLine("ALBA={0}", entry.QData.Timestamp.Sector - 150); //remember to adapt the absolute MSF to an LBA (this field is redundant...)
|
||||
sw.WriteLine("ALBA={0}", entry.QData.Timestamp - 150); //remember to adapt the absolute MSF to an LBA (this field is redundant...)
|
||||
sw.WriteLine("Zero={0}", entry.QData.zero);
|
||||
sw.WriteLine("PMin={0}", entry.QData.ap_min.DecimalValue);
|
||||
sw.WriteLine("PSec={0}", entry.QData.ap_sec.DecimalValue);
|
||||
sw.WriteLine("PFrame={0}", entry.QData.ap_frame.DecimalValue);
|
||||
sw.WriteLine("PLBA={0}", entry.QData.AP_Timestamp.Sector - 150); //remember to adapt the absolute MSF to an LBA (this field is redundant...)
|
||||
sw.WriteLine("PLBA={0}", entry.QData.AP_Timestamp - 150); //remember to adapt the absolute MSF to an LBA (this field is redundant...)
|
||||
sw.WriteLine();
|
||||
}
|
||||
|
||||
|
@ -454,9 +444,9 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
{
|
||||
public void Synth(SectorSynthJob job)
|
||||
{
|
||||
//CCD is always containing everything we'd need (unless a .sub is missing?) so don't about flags
|
||||
var imgBlob = job.Disc.DisposableResources[0] as Disc.Blob_RawFile;
|
||||
var subBlob = job.Disc.DisposableResources[1] as Disc.Blob_RawFile;
|
||||
//CCD is always containing everything we'd need (unless a .sub is missing?) so don't worry about flags
|
||||
var imgBlob = job.Disc.DisposableResources[0] as IBlob;
|
||||
var subBlob = job.Disc.DisposableResources[1] as IBlob;
|
||||
//Read_2442(job.LBA, job.DestBuffer2448, job.DestOffset);
|
||||
|
||||
//read the IMG data if needed
|
||||
|
@ -493,12 +483,48 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
|
||||
Disc disc = new Disc();
|
||||
|
||||
//mount the IMG and SUB files
|
||||
var ccdf = loadResults.ParsedCCDFile;
|
||||
var imgBlob = new Disc.Blob_RawFile() { PhysicalPath = loadResults.ImgPath };
|
||||
var subBlob = new Disc.Blob_RawFile() { PhysicalPath = loadResults.SubPath };
|
||||
IBlob imgBlob = null, subBlob = null;
|
||||
long imgLen = -1, subLen;
|
||||
|
||||
//mount the IMG file
|
||||
//first check for a .ecm in place of the img
|
||||
var imgPath = loadResults.ImgPath;
|
||||
if (!File.Exists(imgPath))
|
||||
{
|
||||
var ecmPath = Path.ChangeExtension(imgPath, ".img.ecm");
|
||||
if (File.Exists(ecmPath))
|
||||
{
|
||||
if (Disc.Blob_ECM.IsECM(ecmPath))
|
||||
{
|
||||
var ecm = new Disc.Blob_ECM();
|
||||
ecm.Load(ecmPath);
|
||||
imgBlob = ecm;
|
||||
imgLen = ecm.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (imgBlob == null)
|
||||
{
|
||||
if (!File.Exists(loadResults.ImgPath)) throw new CCDParseException("Malformed CCD format: nonexistent IMG file!");
|
||||
var imgFile = new Disc.Blob_RawFile() { PhysicalPath = loadResults.ImgPath };
|
||||
imgLen = imgFile.Length;
|
||||
imgBlob = imgFile;
|
||||
}
|
||||
disc.DisposableResources.Add(imgBlob);
|
||||
|
||||
//mount the SUB file
|
||||
if (!File.Exists(loadResults.SubPath)) throw new CCDParseException("Malformed CCD format: nonexistent SUB file!");
|
||||
var subFile = new Disc.Blob_RawFile() { PhysicalPath = loadResults.SubPath };
|
||||
subBlob = subFile;
|
||||
disc.DisposableResources.Add(subBlob);
|
||||
subLen = subFile.Length;
|
||||
|
||||
//quick integrity check of file sizes
|
||||
if (imgLen % 2352 != 0) throw new CCDParseException("Malformed CCD format: IMG file length not multiple of 2352");
|
||||
int NumImgSectors = (int)(imgLen / 2352);
|
||||
if (subLen != NumImgSectors * 96) throw new CCDParseException("Malformed CCD format: SUB file length not matching IMG");
|
||||
|
||||
var ccdf = loadResults.ParsedCCDFile;
|
||||
|
||||
//the only instance of a sector synthesizer we'll need
|
||||
SS_CCD synth = new SS_CCD();
|
||||
|
@ -534,7 +560,7 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
ap_min = BCD2.FromDecimal(entry.PMin),
|
||||
ap_sec = BCD2.FromDecimal(entry.PSec),
|
||||
ap_frame = BCD2.FromDecimal(entry.PFrame),
|
||||
q_crc = 0, //meainingless
|
||||
q_crc = 0, //meaningless
|
||||
};
|
||||
|
||||
disc.RawTOCEntries.Add(new RawTOCEntry { QData = q });
|
||||
|
@ -578,8 +604,8 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
ss_gap.sq.SetStatus(ADR, tocSynth.Result.TOCItems[1].Control);
|
||||
ss_gap.sq.q_tno = BCD2.FromDecimal(1);
|
||||
ss_gap.sq.q_index = BCD2.FromDecimal(0);
|
||||
ss_gap.sq.AP_Timestamp = new Timestamp(i);
|
||||
ss_gap.sq.Timestamp = new Timestamp(qRelMSF);
|
||||
ss_gap.sq.AP_Timestamp = i;
|
||||
ss_gap.sq.Timestamp = qRelMSF;
|
||||
|
||||
//setup subP
|
||||
ss_gap.Pause = true;
|
||||
|
@ -588,7 +614,7 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
//build the sectors:
|
||||
//set up as many sectors as we have img/sub for, even if the TOC doesnt reference them
|
||||
//(the TOC is unreliable, and the Track records are redundant)
|
||||
for (int i = 0; i < loadResults.NumImgSectors; i++)
|
||||
for (int i = 0; i < NumImgSectors; i++)
|
||||
{
|
||||
disc._Sectors.Add(synth);
|
||||
}
|
||||
|
|
|
@ -162,7 +162,7 @@ namespace BizHawk.Emulation.DiscSystem.CUE
|
|||
toc_sq.min = BCD2.FromDecimal(0);
|
||||
toc_sq.sec = BCD2.FromDecimal(0);
|
||||
toc_sq.frame = BCD2.FromDecimal(0);
|
||||
toc_sq.AP_Timestamp = new Timestamp(OUT_Disc._Sectors.Count);
|
||||
toc_sq.AP_Timestamp = OUT_Disc._Sectors.Count;
|
||||
OUT_Disc.RawTOCEntries.Add(new RawTOCEntry { QData = toc_sq });
|
||||
}
|
||||
|
||||
|
@ -322,8 +322,8 @@ namespace BizHawk.Emulation.DiscSystem.CUE
|
|||
ss.sq.SetStatus(ADR, (EControlQ)(int)qTrack.CompiledCueTrack.Flags);
|
||||
ss.sq.q_tno = BCD2.FromDecimal(cct.Number);
|
||||
ss.sq.q_index = BCD2.FromDecimal(curr_index);
|
||||
ss.sq.AP_Timestamp = new Timestamp(OUT_Disc._Sectors.Count);
|
||||
ss.sq.Timestamp = new Timestamp(qRelMSF);
|
||||
ss.sq.AP_Timestamp = OUT_Disc._Sectors.Count;
|
||||
ss.sq.Timestamp = qRelMSF;
|
||||
|
||||
//setup subP
|
||||
if (curr_index == 0)
|
||||
|
@ -363,8 +363,8 @@ namespace BizHawk.Emulation.DiscSystem.CUE
|
|||
ss.sq.SetStatus(ADR, (EControlQ)(int)cct.Flags);
|
||||
ss.sq.q_tno = BCD2.FromDecimal(cct.Number);
|
||||
ss.sq.q_index = BCD2.FromDecimal(curr_index);
|
||||
ss.sq.AP_Timestamp = new Timestamp(OUT_Disc._Sectors.Count);
|
||||
ss.sq.Timestamp = new Timestamp(relMSF);
|
||||
ss.sq.AP_Timestamp = OUT_Disc._Sectors.Count;
|
||||
ss.sq.Timestamp = relMSF;
|
||||
|
||||
//-subP-
|
||||
//always paused--is this good enough?
|
||||
|
@ -383,7 +383,7 @@ namespace BizHawk.Emulation.DiscSystem.CUE
|
|||
IN_FirstRecordedTrackNumber = IN_CompileJob.OUT_CompiledDiscInfo.FirstRecordedTrackNumber,
|
||||
IN_LastRecordedTrackNumber = IN_CompileJob.OUT_CompiledDiscInfo.LastRecordedTrackNumber,
|
||||
IN_Session1Format = IN_CompileJob.OUT_CompiledDiscInfo.SessionFormat,
|
||||
IN_LeadoutTimestamp = new Timestamp(OUT_Disc._Sectors.Count) //do we need a +150?
|
||||
IN_LeadoutTimestamp = OUT_Disc._Sectors.Count
|
||||
};
|
||||
TOCMiscInfo.Run(OUT_Disc.RawTOCEntries);
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
//if (disc.TOC.TOCItems[i].Exists) Console.WriteLine("{0:X8} {1:X2} {2:X2} {3:X8}", crc.Current, (int)disc.TOC.TOCItems[i].Control, disc.TOC.TOCItems[i].Exists ? 1 : 0, disc.TOC.TOCItems[i].LBATimestamp.Sector); //a little debugging
|
||||
crc.Add((int)disc.TOC.TOCItems[i].Control);
|
||||
crc.Add(disc.TOC.TOCItems[i].Exists ? 1 : 0);
|
||||
crc.Add((int)disc.TOC.TOCItems[i].LBATimestamp.Sector);
|
||||
crc.Add((int)disc.TOC.TOCItems[i].LBA);
|
||||
}
|
||||
|
||||
//hash first 26 sectors
|
||||
|
|
|
@ -72,7 +72,7 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
//check track 1's data type. if it's an audio track, further data-track testing is useless
|
||||
//furthermore, it's probably senseless (no binary data there to read)
|
||||
//however a sector could mark itself as audio without actually being.. we'll just wait for that one.
|
||||
if (dsr.ReadLBA_Mode(disc.TOC.TOCItems[1].LBATimestamp.Sector) == 0) return DiscType.AudioDisc;
|
||||
if (dsr.ReadLBA_Mode(disc.TOC.TOCItems[1].LBA) == 0) return DiscType.AudioDisc;
|
||||
|
||||
//sega doesnt put anything identifying in the cdfs volume info. but its consistent about putting its own header here in sector 0
|
||||
if (DetectSegaSaturn()) return DiscType.SegaSaturn;
|
||||
|
|
|
@ -91,19 +91,24 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
/// <summary>
|
||||
/// Retrieves the initial set of timestamps (min,sec,frac) as a convenient Timestamp
|
||||
/// </summary>
|
||||
public Timestamp Timestamp
|
||||
{
|
||||
get { return new Timestamp(min.DecimalValue, sec.DecimalValue, frame.DecimalValue); }
|
||||
set { min.DecimalValue = value.MIN; sec.DecimalValue = value.SEC; frame.DecimalValue = value.FRAC; }
|
||||
public int Timestamp {
|
||||
get { return MSF.ToInt(min.DecimalValue, sec.DecimalValue, frame.DecimalValue); }
|
||||
set {
|
||||
var ts = new Timestamp(value);
|
||||
min.DecimalValue = ts.MIN; sec.DecimalValue = ts.SEC; frame.DecimalValue = ts.FRAC;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the second set of timestamps (ap_min, ap_sec, ap_frac) as a convenient Timestamp.
|
||||
/// TODO - rename everything AP here, it's nonsense. (the P is)
|
||||
/// </summary>
|
||||
public Timestamp AP_Timestamp {
|
||||
get { return new Timestamp(ap_min.DecimalValue, ap_sec.DecimalValue, ap_frame.DecimalValue); }
|
||||
set { ap_min.DecimalValue = value.MIN; ap_sec.DecimalValue = value.SEC; ap_frame.DecimalValue = value.FRAC; }
|
||||
public int AP_Timestamp {
|
||||
get { return MSF.ToInt(ap_min.DecimalValue, ap_sec.DecimalValue, ap_frame.DecimalValue); }
|
||||
set {
|
||||
var ts = new Timestamp(value);
|
||||
ap_min.DecimalValue = ts.MIN; ap_sec.DecimalValue = ts.SEC; ap_frame.DecimalValue = ts.FRAC;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
/// <summary>
|
||||
/// The location of the track (Index 1)
|
||||
/// </summary>
|
||||
public Timestamp LBATimestamp;
|
||||
public int LBA;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this entry exists (since the table is 101 entries long always)
|
||||
|
@ -63,7 +63,7 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
/// <summary>
|
||||
/// The timestamp of the leadout track. In other words, the end of the user area.
|
||||
/// </summary>
|
||||
public Timestamp LeadoutLBA { get { return TOCItems[100].LBATimestamp; } }
|
||||
public int LeadoutLBA { get { return TOCItems[100].LBA; } }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -81,6 +81,14 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
}
|
||||
}
|
||||
|
||||
public static class MSF
|
||||
{
|
||||
public static int ToInt(int m, int s, int f)
|
||||
{
|
||||
return m * 60 * 75 + s * 75 + f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// todo - rename to MSF? It can specify durations, so maybe it should be not suggestive of timestamp
|
||||
/// TODO - can we maybe use BCD2 in here
|
||||
|
|
|
@ -27,7 +27,7 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
/// <summary>
|
||||
/// The absolute timestamp of the lead-out track
|
||||
/// </summary>
|
||||
public Timestamp IN_LeadoutTimestamp;
|
||||
public int IN_LeadoutTimestamp;
|
||||
|
||||
/// <summary>
|
||||
/// The session format for this TOC
|
||||
|
|
|
@ -40,7 +40,7 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
{
|
||||
Number = i + 1,
|
||||
Control = item.Control,
|
||||
LBA = item.LBATimestamp.Sector
|
||||
LBA = item.LBA
|
||||
};
|
||||
session.Tracks.Add(track);
|
||||
|
||||
|
@ -66,7 +66,7 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
//kind of a guess, but not completely
|
||||
Control = session.Tracks[session.Tracks.Count -1 ].Control,
|
||||
Mode = session.Tracks[session.Tracks.Count - 1].Mode,
|
||||
LBA = TOCRaw.LeadoutLBA.Sector
|
||||
LBA = TOCRaw.LeadoutLBA
|
||||
});
|
||||
|
||||
//link track list
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
DiscTOC ret = new DiscTOC();
|
||||
|
||||
//this is a dummy, for convenience in array indexing, so that track 1 is at array index 1
|
||||
ret.TOCItems[0].LBATimestamp = new Timestamp(0); //arguably could be -150, but let's not just yet
|
||||
ret.TOCItems[0].LBA = 0; //arguably could be -150, but let's not just yet
|
||||
ret.TOCItems[0].Control = 0;
|
||||
ret.TOCItems[0].Exists = false;
|
||||
|
||||
|
@ -44,7 +44,7 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
else if (point <= 99)
|
||||
{
|
||||
maxFoundTrack = Math.Max(maxFoundTrack, point);
|
||||
ret.TOCItems[point].LBATimestamp = new Timestamp(q.AP_Timestamp.Sector - 150); //RawTOCEntries contained an absolute time
|
||||
ret.TOCItems[point].LBA = q.AP_Timestamp - 150; //RawTOCEntries contained an absolute time
|
||||
ret.TOCItems[point].Control = q.CONTROL;
|
||||
ret.TOCItems[point].Exists = true;
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
}
|
||||
else if (point == 102) //0xA2 bcd
|
||||
{
|
||||
ret.TOCItems[100].LBATimestamp = new Timestamp(q.AP_Timestamp.Sector - 150); //RawTOCEntries contained an absolute time
|
||||
ret.TOCItems[100].LBA = q.AP_Timestamp - 150; //RawTOCEntries contained an absolute time
|
||||
ret.TOCItems[100].Control = 0; //not clear what this should be
|
||||
ret.TOCItems[100].Exists = true;
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
sq.sec = BCD2.FromDecimal(new Timestamp(track_relative_msf).SEC);
|
||||
sq.frame = BCD2.FromDecimal(new Timestamp(track_relative_msf).FRAC);
|
||||
|
||||
int absolute_msf = i + leadoutTs.Sector;
|
||||
int absolute_msf = i + leadoutTs;
|
||||
sq.ap_min = BCD2.FromDecimal(new Timestamp(absolute_msf + 150).MIN);
|
||||
sq.ap_sec = BCD2.FromDecimal(new Timestamp(absolute_msf + 150).SEC);
|
||||
sq.ap_frame = BCD2.FromDecimal(new Timestamp(absolute_msf + 150).FRAC);
|
||||
|
|
|
@ -229,8 +229,8 @@ namespace BizHawk.Emulation.DiscSystem
|
|||
|
||||
//data is zero
|
||||
|
||||
Timestamp ts = new Timestamp(lba_relative);
|
||||
Timestamp ats = new Timestamp(job.LBA);
|
||||
int ts = lba_relative;
|
||||
int ats = job.LBA;
|
||||
|
||||
const int ADR = 0x1; // Q channel data encodes position
|
||||
EControlQ control = ses.LeadoutTrack.Control;
|
||||
|
|
Loading…
Reference in New Issue