1 |
#define READ_RESULTS_ON_CREATION // when defined will call GetResults() from ReadHeader() |
2 |
#region Logging Defines |
3 |
// include this any class or method that required logging, and comment-out what is not needed |
4 |
|
5 |
#region Enabled logging levels |
6 |
#define LOGGING_ENABLE_INFO |
7 |
#define LOGGING_ENABLE_WARN |
8 |
#define LOGGING_ENABLE_DEBUG |
9 |
#define LOGGING_ENABLE_VERBOSEDEBUG |
10 |
#define LOGGING_ENABLE_ERROR |
11 |
#define LOGGING_ENABLE_VERBOSEERROR |
12 |
#define LOGGING_ENABLE_PROFILER |
13 |
#endregion |
14 |
#endregion |
15 |
using System; |
16 |
using System.Collections.Generic; |
17 |
using System.Linq; |
18 |
using System.Text; |
19 |
using RomCheater.Docking.MemorySearch; |
20 |
using RomCheater.Logging; |
21 |
using System.IO; |
22 |
using Utilities.TransparentControls; |
23 |
using Sojaner.MemoryScanner.MemoryProviers; |
24 |
|
25 |
using System.Windows.Forms; |
26 |
using RomCheater.Core; |
27 |
using System.Diagnostics; |
28 |
using System.Collections; |
29 |
using Enterprise.Logging; |
30 |
|
31 |
namespace RomCheater.Serialization |
32 |
{ |
33 |
public interface ISearchResultReader : ISerializedResult |
34 |
{ |
35 |
//ResultType<TValue> GetNextResult<TValue>() where TValue : IConvertible; |
36 |
} |
37 |
|
38 |
public interface ISerializedResult : ISerializedResult<ulong> { } |
39 |
public interface ISerializedResult<T> where T: IConvertible |
40 |
{ |
41 |
//ResultReaderCollection GetResultCollection(); |
42 |
bool ContainsAddress(uint address); |
43 |
bool ContainsAddress(uint address,out int index); |
44 |
void GetResultAtIndex(int index, out StructResultType<T> result); |
45 |
void UpdateResultValuesFromMemory(ref StructResultType<T>[] results, IAcceptsProcessAndConfig iapc); |
46 |
} |
47 |
|
48 |
|
49 |
|
50 |
#region public class ResultReaderEnumerator : IEnumerator<StructResultType<ulong>> |
51 |
//public class ResultReaderEnumerator : IEnumerator<StructResultType<ulong>> |
52 |
//{ |
53 |
// //private IEnumerator<StructResultType<ulong>> _enumerator; |
54 |
// private int _index = 0; |
55 |
// private int index { get { return _index; } set { _index = value; } } |
56 |
// private SearchResultReader _reader; |
57 |
// private SearchResultReader reader { get { CheckReaderNull(); return _reader; } set { _reader = value; CheckReaderNull(); } } |
58 |
// private int MinimumIndex { get { return this.reader.MinimumIndex; } } |
59 |
// private int MaximumIndex { get { return this.reader.MaximumIndex; } } |
60 |
// #region Exceptions |
61 |
// private void CheckReaderNull() |
62 |
// { |
63 |
// if (_reader == null) |
64 |
// { |
65 |
// throw new ArgumentNullException("reader", "Reader cannot be null"); |
66 |
// } |
67 |
// } |
68 |
// private void CheckIndexLessThanMin() |
69 |
// { |
70 |
// if (_index < MinimumIndex) |
71 |
// { |
72 |
// throw new ArgumentOutOfRangeException("index", string.Format("index cannot be less than {0}", MinimumIndex)); |
73 |
// } |
74 |
// } |
75 |
// private void CheckIndexGreaterThanMax() |
76 |
// { |
77 |
// if (_index > MaximumIndex) |
78 |
// { |
79 |
// throw new ArgumentOutOfRangeException("index", string.Format("index cannot be greater than {0}", MaximumIndex)); |
80 |
// } |
81 |
// } |
82 |
// #endregion |
83 |
// public ResultReaderEnumerator(SearchResultReader reader) |
84 |
// { |
85 |
// this.reader = reader; |
86 |
// this.index = -1; |
87 |
// } |
88 |
// public StructResultType<ulong> Current |
89 |
// { |
90 |
// get |
91 |
// { |
92 |
// // result the current entry (at position) |
93 |
// //throw new NotImplementedException(); |
94 |
// CheckIndexLessThanMin(); |
95 |
// CheckIndexGreaterThanMax(); |
96 |
// StructResultType<ulong> result = StructResultType<ulong>.Empty; |
97 |
// reader.GetResultAtIndex(this.index, out result); |
98 |
// return result; |
99 |
// } |
100 |
// } |
101 |
// public void Dispose() |
102 |
// { |
103 |
// //throw new NotImplementedException(); |
104 |
// } |
105 |
// object System.Collections.IEnumerator.Current |
106 |
// { |
107 |
// get { return this.Current; } |
108 |
// } |
109 |
// public bool MoveNext() |
110 |
// { |
111 |
// this.index++; // increment after processing index at MinimumIndex |
112 |
// if (this.index >= this.MinimumIndex && this.index <= this.MaximumIndex) |
113 |
// { |
114 |
// return true; |
115 |
// } |
116 |
// Reset(); |
117 |
// return false; |
118 |
// } |
119 |
// public void Reset() |
120 |
// { |
121 |
// this.index = -1; |
122 |
// } |
123 |
//} |
124 |
#endregion |
125 |
|
126 |
#region public class ResultReaderCollection : IEnumerable<StructResultType<ulong>> |
127 |
//public class ResultReaderCollection : IEnumerable<StructResultType<ulong>> |
128 |
//{ |
129 |
// private SearchResultReader _reader; |
130 |
// private SearchResultReader reader { get { CheckReaderNull(); return _reader; } set { _reader = value; CheckReaderNull(); } } |
131 |
// public ResultReaderCollection(SearchResultReader reader) |
132 |
// { |
133 |
// this.reader = reader; |
134 |
// } |
135 |
// #region Exceptions |
136 |
// private void CheckReaderNull() |
137 |
// { |
138 |
// if (_reader == null) |
139 |
// { |
140 |
// throw new ArgumentNullException("reader", "Reader cannot be null"); |
141 |
// } |
142 |
// } |
143 |
// #endregion |
144 |
// public IEnumerator<StructResultType<ulong>> GetEnumerator() |
145 |
// { |
146 |
// return new ResultReaderEnumerator(_reader); |
147 |
// } |
148 |
// System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() |
149 |
// { |
150 |
// return GetEnumerator(); |
151 |
// } |
152 |
//} |
153 |
#endregion |
154 |
|
155 |
|
156 |
|
157 |
#region public class SearchResultReader : SerializationReader, ISearchResultReader, ISerializedResult |
158 |
public class SearchResultReader : SerializationReader, ISearchResultReader, ISerializedResult<ulong>, IEnumerator<StructResultType<ulong>>, IEnumerable<StructResultType<ulong>> |
159 |
{ |
160 |
//private Guid _ResultGuid; |
161 |
//private Guid ResultGuid { get { return _ResultGuid; } set { _ResultGuid = value; } } |
162 |
public ulong MinimumIndex { get { return 0; } } |
163 |
public ulong MaximumIndex { get { return this.ResultCount - 1; } } |
164 |
|
165 |
private ulong ResultDataOffset = 0; |
166 |
////private long CurrentResultOffset = 0; |
167 |
////public SearchResultReader() : base() { ReadHeader(); } |
168 |
//[Obsolete("SearchResultReader(Guid guid) should not be used")] |
169 |
//public SearchResultReader(Guid guid) : base(guid, false) { ResultGuid = guid; ReadHeader(); } |
170 |
//[Obsolete("SearchResultReader(Guid guid, bool delete) should not be used")] |
171 |
//public SearchResultReader(Guid guid, bool delete) : base(guid, delete) { ResultGuid = guid; ReadHeader(); } |
172 |
|
173 |
private void CheckIndexLessThanMin() |
174 |
{ |
175 |
if (_index < MinimumIndex) |
176 |
{ |
177 |
throw new ArgumentOutOfRangeException("index", string.Format("index cannot be less than {0}", MinimumIndex)); |
178 |
} |
179 |
} |
180 |
private void CheckIndexGreaterThanMax() |
181 |
{ |
182 |
if (_index > MaximumIndex) |
183 |
{ |
184 |
throw new ArgumentOutOfRangeException("index", string.Format("index cannot be greater than {0}", MaximumIndex)); |
185 |
} |
186 |
} |
187 |
private ulong _index = 0; |
188 |
private ulong index { get { return _index; } set { _index = value; } } |
189 |
|
190 |
#region IEnumerable<StructResultType<ulong>> members |
191 |
public IEnumerator<StructResultType<ulong>> GetEnumerator() |
192 |
{ |
193 |
return this; // we are implementing our own custom enumerator through use of: IEnumerator<StructResultType<ulong>> |
194 |
} |
195 |
|
196 |
IEnumerator IEnumerable.GetEnumerator() |
197 |
{ |
198 |
return this.GetEnumerator(); |
199 |
} |
200 |
#endregion |
201 |
|
202 |
#region IEnumerator<StructResultType<ulong>> members |
203 |
public StructResultType<ulong> Current |
204 |
{ |
205 |
get |
206 |
{ |
207 |
// result the current entry (at position) |
208 |
//throw new NotImplementedException(); |
209 |
CheckIndexLessThanMin(); |
210 |
CheckIndexGreaterThanMax(); |
211 |
return results[index]; |
212 |
} |
213 |
} |
214 |
object IEnumerator.Current |
215 |
{ |
216 |
get { return this.Current; } |
217 |
} |
218 |
|
219 |
public bool MoveNext() |
220 |
{ |
221 |
this.index++; // increment after processing index at MinimumIndex |
222 |
if (this.index >= this.MinimumIndex && this.index <= this.MaximumIndex) |
223 |
{ |
224 |
return true; |
225 |
} |
226 |
Reset(); |
227 |
return false; |
228 |
} |
229 |
|
230 |
public void Reset() |
231 |
{ |
232 |
this.index = 0; |
233 |
} |
234 |
#endregion |
235 |
|
236 |
//#region IEnumerator<StructResultType<ulong>> members |
237 |
//public IEnumerator<StructResultType<ulong>> GetEnumerator() |
238 |
//{ |
239 |
// return (IEnumerator<StructResultType<ulong>>)results.GetEnumerator(); |
240 |
//} |
241 |
//System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() |
242 |
//{ |
243 |
// return GetEnumerator(); |
244 |
//} |
245 |
//#endregion |
246 |
#region ISerializedResult<T> members |
247 |
//public ResultReaderCollection GetResultCollection() |
248 |
//{ |
249 |
// ResultReaderCollection collection = new ResultReaderCollection(this); |
250 |
// return collection; |
251 |
//} |
252 |
public bool ContainsAddress(uint address) |
253 |
{ |
254 |
int index = 0; |
255 |
return ContainsAddress(address, out index); |
256 |
} |
257 |
public bool ContainsAddress(uint address, out int index) |
258 |
{ |
259 |
bool contains = false; |
260 |
index = 0; |
261 |
foreach (var k in this.results) |
262 |
{ |
263 |
if (k.Address == address) |
264 |
{ |
265 |
contains = true; |
266 |
break; |
267 |
} |
268 |
index++; |
269 |
} |
270 |
return contains; |
271 |
|
272 |
|
273 |
//#error ContainsAddress - Not Implemented - need update for change to GetResults() |
274 |
|
275 |
// foreach (var result in GetResults()) |
276 |
// { |
277 |
// if (cancel_method.Invoke()) { break; } |
278 |
// if (result.Address == address) |
279 |
// { |
280 |
// if (contains) |
281 |
// { |
282 |
// throw new ArgumentOutOfRangeException("address", string.Format("Found more than one result that matched address: 0x{0} - index={1}", address.ToString("X"), index)); |
283 |
// } |
284 |
// contains = true; |
285 |
// break; // should never throw the exception, if we break here (only returning/checking the first found address |
286 |
// } |
287 |
// index++; |
288 |
// } |
289 |
|
290 |
} |
291 |
public void GetResultAtIndex(int index, out StructResultType<ulong> result) |
292 |
{ |
293 |
result = StructResultType<ulong>.Empty; |
294 |
result = this.results[index]; |
295 |
//#error GetResultAtIndex - Not Implemented - need update for change to GetResults() |
296 |
// int count = 0; |
297 |
// foreach (var entry in GetResults()) |
298 |
// { |
299 |
// if (index == count) |
300 |
// { |
301 |
// result = entry; |
302 |
// break; |
303 |
// } |
304 |
// count++; |
305 |
// } |
306 |
} |
307 |
|
308 |
public void UpdateResultValuesFromMemory(ref StructResultType<ulong>[] results, IAcceptsProcessAndConfig iapc) |
309 |
{ |
310 |
Stopwatch st = new Stopwatch(); |
311 |
st.Start(); |
312 |
List<ulong> addresses = new List<ulong>(); |
313 |
results.ToList().ForEach(s => addresses.Add(s.Address)); |
314 |
using (GenericMemoryProvider provider = new GenericMemoryProvider(iapc)) |
315 |
{ |
316 |
byte[][] values; |
317 |
provider.OpenProvider(); |
318 |
provider.UpdateAddressArray(addresses.ToArray(), BitTools.SizeOf<uint>(datatype), out values); |
319 |
provider.CloseProvider(); |
320 |
|
321 |
for (int i = 0; i < results.Length; i++) |
322 |
{ |
323 |
if (this.cancel_method.Invoke()) |
324 |
{ |
325 |
gLog.Warn.WriteLine("UpdateResultValuesFromMemory - operation cancelled at [ResultIndex=0x{0} ChunkCount=0x{1}]", i.ToString("X"), results.Length.ToString("X")); |
326 |
break; |
327 |
} |
328 |
ulong value = 0; |
329 |
byte[] data = values[i]; |
330 |
switch (datatype) |
331 |
{ |
332 |
case SearchDataTypes._8bits: value = data[0]; break; |
333 |
case SearchDataTypes._16bits: value = BitConverter.ToUInt16(data, 0); break; |
334 |
case SearchDataTypes._32bits: value = BitConverter.ToUInt32(data, 0); break; |
335 |
case SearchDataTypes._64bits: value = BitConverter.ToUInt64(data, 0); break; |
336 |
} |
337 |
results[i].Value = value; |
338 |
gLog.Profiler.WriteLine("UpdateResultValuesFromMemory [ResultIndex=0x{0} ChunkCount=0x{1}] has taken a total of {2} seconds to complete", i.ToString("X"), results.Length.ToString("X"), st.Elapsed.TotalSeconds.ToString()); |
339 |
|
340 |
|
341 |
string message = string.Format(" -> Updateing values [ResultIndex=0x{0} of ChunkCount=0x{1}]", i.ToString("X"), results.Length.ToString("X")); |
342 |
double double_percent_done = 100.0 * (double)((double)i / (double)results.Length); |
343 |
if (((double)i % ((double)results.Length) * 0.25) == 0) |
344 |
{ |
345 |
update_progress.Invoke((int)double_percent_done, message); |
346 |
} |
347 |
} |
348 |
st.Stop(); |
349 |
gLog.Profiler.WriteLine("UpdateResultValuesFromMemory [ChunkCount=0x{0}] took a total of {1} seconds to complete", results.Length.ToString("X"), st.Elapsed.TotalSeconds.ToString()); |
350 |
} |
351 |
} |
352 |
|
353 |
#endregion |
354 |
|
355 |
private StructResultType<ulong>[] results = new StructResultType<ulong>[0]; |
356 |
|
357 |
private bool _unsigned; |
358 |
private bool unsigned { get { return _unsigned; } set { _unsigned = value; } } |
359 |
private SearchDataTypes _datatype; |
360 |
private SearchDataTypes datatype { get { return _datatype; } set { _datatype = value; } } |
361 |
private Action<int, string> _update_progress; |
362 |
private Action<int, string> update_progress { get { return _update_progress; } set { _update_progress = value; } } |
363 |
private Func<bool> _cancel_method; |
364 |
private Func<bool> cancel_method { get { return _cancel_method; } set { _cancel_method = value; } } |
365 |
|
366 |
public SearchResultReader(Guid guid, bool unsigned, SearchDataTypes datatype, Action<int, string> update_progress, Func<bool> cancelmethod) : this(guid, false, unsigned, datatype, update_progress, cancelmethod) { } |
367 |
public SearchResultReader(Guid guid, bool delete, bool unsigned, SearchDataTypes datatype, Action<int, string> update_progress, Func<bool> cancelmethod) |
368 |
: base(guid, delete) |
369 |
{ |
370 |
this.unsigned = unsigned; |
371 |
this.datatype = datatype; |
372 |
this.update_progress = update_progress; |
373 |
this.cancel_method = cancel_method; |
374 |
ReaderGuid = guid; |
375 |
ReadHeader(); |
376 |
} |
377 |
|
378 |
|
379 |
public void ReleaseMemory() |
380 |
{ |
381 |
this.results = null; |
382 |
} |
383 |
|
384 |
protected override void Dispose(bool disposing) |
385 |
{ |
386 |
base.Dispose(disposing); |
387 |
this.ReleaseMemory(); |
388 |
} |
389 |
|
390 |
protected override string TemporaryFolder { get { return SearchResultsConstants.SearchResultsFolder; } } |
391 |
private void ReadHeader() |
392 |
{ |
393 |
try |
394 |
{ |
395 |
using (FileStream fs = CreateReader()) |
396 |
{ |
397 |
using (BinaryReader binReader = new BinaryReader(fs)) |
398 |
{ |
399 |
//int ResultsRead = 0; |
400 |
// SRD (string) |
401 |
string magic = Encoding.UTF8.GetString(binReader.ReadBytes(3)); |
402 |
string SRD = "SRD"; |
403 |
if (magic != SRD) |
404 |
{ |
405 |
throw new InvalidOperationException(string.Format("Encountered unexpected magic: {0} expected: {1}", magic, SRD)); |
406 |
} |
407 |
// version (int) |
408 |
int version = binReader.ReadInt32(); |
409 |
|
410 |
if (version == 1) |
411 |
{ |
412 |
// do nothing |
413 |
} |
414 |
else if (version == 2) |
415 |
{ |
416 |
int guid_array_length = binReader.ReadInt32(); |
417 |
byte[] guid_array = new byte[guid_array_length]; |
418 |
binReader.Read(guid_array, 0, guid_array_length); |
419 |
Guid g = new Guid(guid_array); |
420 |
if (g != ReaderGuid) |
421 |
{ |
422 |
throw new InvalidOperationException(string.Format("Encountered wrong search results guid: read '{1}' excpected '{2}'", g.ToString(), ReaderGuid.ToString())); |
423 |
} |
424 |
} |
425 |
else |
426 |
{ |
427 |
throw new InvalidOperationException(string.Format("Encountered unexpected version: {0} expected: {1} or {2}", version, 1, 2)); |
428 |
} |
429 |
// resultcount |
430 |
ulong resultcount = binReader.ReadUInt64(); |
431 |
if (resultcount == 0) |
432 |
{ |
433 |
throw new InvalidOperationException(string.Format("Result Count is zero")); |
434 |
} |
435 |
ResultCount = resultcount; |
436 |
ResultDataOffset = (ulong)binReader.BaseStream.Position; |
437 |
|
438 |
|
439 |
#if READ_RESULTS_ON_CREATION |
440 |
// this opperation may use alot of memory |
441 |
GetResults(binReader); |
442 |
#endif |
443 |
|
444 |
binReader.Close(); |
445 |
} |
446 |
fs.Close(); |
447 |
} |
448 |
} |
449 |
catch (System.IO.EndOfStreamException) { } |
450 |
} |
451 |
#region ISearchResultReader members |
452 |
public bool ReadCurrentAddess { get; private set; } |
453 |
public bool ReadCurrentValue { get; private set; } |
454 |
|
455 |
//[Obsolete("GetNextResult has been replaced by GetResults")] |
456 |
//public ResultType<TValue> GetNextResult<TValue>() where TValue : IConvertible |
457 |
//{ |
458 |
// return new ResultType<TValue>(); |
459 |
//} |
460 |
#endregion |
461 |
|
462 |
|
463 |
#region |
464 |
private void GetResults(BinaryReader br) |
465 |
{ |
466 |
results = new StructResultType<ulong>[ResultCount]; |
467 |
try |
468 |
{ |
469 |
for (ulong i = 0; i < ResultCount; i++) |
470 |
{ |
471 |
StructResultType<ulong> result = StructResultType<ulong>.Empty; |
472 |
ulong Address = br.ReadUInt64(); |
473 |
ulong Value = 0; |
474 |
switch (datatype) |
475 |
{ |
476 |
case SearchDataTypes._8bits: |
477 |
if (unsigned) { Value = br.ReadByte(); } else { Value = (ulong)br.ReadSByte(); } |
478 |
break; |
479 |
case SearchDataTypes._16bits: |
480 |
if (unsigned) { Value = br.ReadUInt16(); } else { Value = (ulong)br.ReadInt16(); } |
481 |
break; |
482 |
case SearchDataTypes._32bits: |
483 |
if (unsigned) { Value = br.ReadUInt32(); } else { Value = (ulong)br.ReadInt32(); } |
484 |
break; |
485 |
case SearchDataTypes._64bits: |
486 |
if (unsigned) { Value = br.ReadUInt64(); } else { Value = (ulong)br.ReadInt64(); } |
487 |
break; |
488 |
} |
489 |
result = new StructResultType<ulong>(Address, Value); |
490 |
results[i] = result; |
491 |
} |
492 |
//results.TrimExcess(); |
493 |
} |
494 |
catch (Exception ex) |
495 |
{ |
496 |
gLog.Error.WriteLine("Failed to reader results..."); |
497 |
gLog.Verbose.Error.WriteLine(ex.ToString()); |
498 |
results =new StructResultType<ulong>[0]; |
499 |
//results.TrimExcess(); |
500 |
throw ex; |
501 |
} |
502 |
} |
503 |
#endregion |
504 |
|
505 |
//#region private StructResultType<ulong> GetResultAtIndex(int index) |
506 |
//private StructResultType<ulong> GetResultAtIndex(int index) |
507 |
//{ |
508 |
// try |
509 |
// { |
510 |
// //update_progress.Invoke(0, string.Empty); |
511 |
// int data_size = sizeof(uint); // address size ... should be 4 bytes (could be 8, if we have a 64-bit address) |
512 |
// StructResultType<ulong> result = StructResultType<ulong>.Empty; |
513 |
// if (cancel_method.Invoke()) { return result; } |
514 |
// using (FileStream fs = this.CreateReader()) |
515 |
// { |
516 |
// using (BinaryReader binReader = new BinaryReader(fs)) |
517 |
// { |
518 |
// binReader.BaseStream.Seek(this.ResultDataOffset, SeekOrigin.Begin); // seek to start of result data |
519 |
// data_size += (int)datatype / 8; |
520 |
// long offset = data_size * index; |
521 |
// binReader.BaseStream.Seek(offset, SeekOrigin.Current); |
522 |
// uint Address = binReader.ReadUInt32(); |
523 |
// ulong Value = 0; |
524 |
// switch (datatype) |
525 |
// { |
526 |
// case SearchDataTypes._8bits: |
527 |
// if (unsigned) { Value = binReader.ReadByte(); } else { Value = (ulong)binReader.ReadSByte(); } |
528 |
// break; |
529 |
// case SearchDataTypes._16bits: |
530 |
// if (unsigned) { Value = binReader.ReadUInt16(); } else { Value = (ulong)binReader.ReadInt16(); } |
531 |
// break; |
532 |
// case SearchDataTypes._32bits: |
533 |
// if (unsigned) { Value = binReader.ReadUInt32(); } else { Value = (ulong)binReader.ReadInt32(); } |
534 |
// break; |
535 |
// case SearchDataTypes._64bits: |
536 |
// if (unsigned) { Value = binReader.ReadUInt64(); } else { Value = (ulong)binReader.ReadInt64(); } |
537 |
// break; |
538 |
// } |
539 |
// result = new StructResultType<ulong>(Address, Value); |
540 |
// } |
541 |
// } |
542 |
// //double percent_done = 100.0 * ((double)index / (double)reader.ResultCount); |
543 |
// //string message = string.Format("-> Reading Result at index: 0x{0}", index.ToString("X")); |
544 |
// //update_progress((int)percent_done, message); |
545 |
// return result; |
546 |
// } |
547 |
// catch (Exception ex) |
548 |
// { |
549 |
// logger.Error.WriteLine("GetResultAtIndex...Failed to read the stream at index: 0x{0}", index.ToString("X")); |
550 |
// logger.VerboseError.WriteLine(ex.ToString()); |
551 |
// return StructResultType<ulong>.Empty; |
552 |
// } |
553 |
//} |
554 |
//#endregion |
555 |
} |
556 |
#endregion |
557 |
} |