using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using BizHawk.Common.NumberExtensions; using BizHawk.Common.StringExtensions; using BizHawk.Emulation.Common; namespace BizHawk.Client.Common { /// /// This class hold a collection /// Different memory domain can be mixed /// public sealed partial class WatchList : IList { #region Fields public const string ADDRESS = "AddressColumn"; public const string VALUE = "ValueColumn"; public const string PREV = "PrevColumn"; public const string CHANGES = "ChangesColumn"; public const string DIFF = "DiffColumn"; public const string DOMAIN = "DomainColumn"; public const string NOTES = "NotesColumn"; private static readonly WatchDomainComparer domainComparer = new WatchDomainComparer(); private static readonly WatchAddressComparer addressComparer = new WatchAddressComparer(); private static readonly WatchValueComparer valueComparer = new WatchValueComparer(); private static readonly WatchPreviousValueComparer previousValueComparer = new WatchPreviousValueComparer(); private static readonly WatchValueDifferenceComparer valueDifferenceComparer = new WatchValueDifferenceComparer(); private static readonly WatchChangeCountComparer changeCountComparer = new WatchChangeCountComparer(); private static readonly WatchNoteComparer noteComparer = new WatchNoteComparer(); private static IMemoryDomains _memoryDomains; private List _watchList = new List(0); private MemoryDomain _domain; private string _currentFilename = string.Empty; private string _systemid; #endregion #region cTor(s) /// /// Initialize a new instance of that will /// contains a set of /// /// All available memomry domains /// Domain you want to watch /// System identifier (NES, SNES, ...) [Obsolete("Use the constructor with two parameters instead")] public WatchList(IMemoryDomains core, MemoryDomain domain, string systemid) { if (_memoryDomains == null) { _memoryDomains = core; } _domain = domain; _systemid = systemid; } /// /// Initialize a new instance of that will /// contains a set of /// /// All available memomry domains /// Domain you want to watch /// System identifier (NES, SNES, ...) public WatchList(IMemoryDomains core, string systemid) { if (_memoryDomains == null) { _memoryDomains = core; } //TODO: Remove this after tests _domain = core.MainMemory; _systemid = systemid; } #endregion #region Methods #region ICollection /// /// Adds a into the current collection /// /// to add public void Add(Watch watch) { _watchList.Add(watch); Changes = true; } /// /// Removes all item from the current collection /// Clear also the file name /// public void Clear() { _watchList.Clear(); Changes = false; _currentFilename = string.Empty; } /// /// Determines if the current contains the /// specified /// /// The object to /// public bool Contains(Watch watch) { return _watchList.Contains(watch); } /// /// Copies the elements of the current /// into an starting at a particular index /// /// The one-dimension that will serve as destination to copy /// Zero-based index where the copy should starts /// /// /// public void CopyTo(Watch[] array, int arrayIndex) { _watchList.CopyTo(array, arrayIndex); } /// /// Removes the first of specified /// /// to remove /// True if successfully removed; otherwise, false public bool Remove(Watch watch) { bool result = _watchList.Remove(watch); if (result) { Changes = true; } return result; } #endregion #region IList /// /// Determines the zero-base position of the specified /// into the /// /// to look for /// Zero-base position if has been found; otherwise -1 public int IndexOf(Watch watch) { return _watchList.IndexOf(watch); } /// /// Insert a at the specified index /// /// The zero-base index where the should be inserted /// to insert /// public void Insert(int index, Watch watch) { _watchList.Insert(index, watch); } /// /// Removes item at the specified index /// /// Zero-based index of the to remove /// public void RemoveAt(int index) { _watchList.RemoveAt(index); Changes = true; } #endregion IList #region IEnumerable /// /// Returns an enumerator that iterates through the collection /// /// An for the current collection public IEnumerator GetEnumerator() { return _watchList.GetEnumerator(); } /// /// Returns an enumerator that iterates through the collection /// /// An for the current collection IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion IEnumerable /// /// Add an existing collection of into the current one /// equality will be checked to avoid doubles /// /// of watch to merge public void AddRange(IEnumerable watches) { Parallel.ForEach(watches, watch => { if (!_watchList.Contains(watch)) { _watchList.Add(watch); } }); Changes = true; } /// /// Clears change count of all in the collection /// public void ClearChangeCounts() { Parallel.ForEach(_watchList, watch => watch.ClearChangeCount()); } /// /// Sort the current list based on one of the constant /// /// Value that specify sorting base /// Value that define the ordering. Ascending (true) or desceding (false) public void OrderWatches(string column, bool reverse) { switch (column) { case ADDRESS: if (reverse) { _watchList.Sort(addressComparer); _watchList.Reverse(); } else { _watchList.Sort(); } break; case VALUE: if (reverse) { _watchList.Sort(valueComparer); _watchList.Reverse(); } else { _watchList.Sort(valueComparer); } break; case PREV: if (reverse) { _watchList.Sort(previousValueComparer); _watchList.Reverse(); } else { _watchList.Sort(previousValueComparer); } break; case DIFF: if (reverse) { _watchList.Sort(valueDifferenceComparer); _watchList.Reverse(); } else { _watchList.Sort(valueDifferenceComparer); } break; case CHANGES: if (reverse) { _watchList.Sort(changeCountComparer); _watchList.Reverse(); } else { _watchList.Sort(changeCountComparer); } break; case DOMAIN: if (reverse) { _watchList.Sort(domainComparer); _watchList.Reverse(); } else { _watchList.Sort(domainComparer); } break; case NOTES: if (reverse) { _watchList.Sort(noteComparer); _watchList.Reverse(); } else { _watchList.Sort(noteComparer); } break; } } /// /// Sets WatchList's domain list to a new one /// domain will also be refreshed /// /// New domains public void RefreshDomains(IMemoryDomains core) { _memoryDomains = core; Parallel.ForEach(_watchList, watch => { watch.Domain = core[watch.Domain.Name]; watch.ResetPrevious(); watch.Update(); watch.ClearChangeCount(); } ); } /// /// Updates all ine the current collection /// public void UpdateValues() { Parallel.ForEach(_watchList, watch => { watch.Update(); }); } public string AddressFormatStr // TODO: this is probably compensating for not using the ToHex string extension { get { if (_domain != null) { return "{0:X" + (_domain.Size - 1).NumHexDigits() + "}"; } return string.Empty; } } #endregion #region Propeties #region ICollection /// /// Gets the number of elements contained in this /// public int Count { get { return _watchList.Count; } } /// /// is alsways read-write /// so this value will be always false /// public bool IsReadOnly { get { return false; } } #endregion ICollection #region IList /// /// Gets or sets element at the specified index /// /// The zero based index of the element you want to get or set /// at the specified index public Watch this[int index] { get { return _watchList[index]; } set { _watchList[index] = value; } } #endregion IList /// /// Gets a value indicating if collection has changed or not /// public bool Changes { get; set; } /// /// Gets or sets current 's filename /// public string CurrentFileName { get { return _currentFilename; } set { _currentFilename = value; } } /// /// Gets the number of that are not /// public int WatchCount { get { return _watchList.Count(watch => !watch.IsSeparator); } } #endregion [Obsolete("Use the method with single parameter instead")] public void RefreshDomains(IMemoryDomains core, MemoryDomain domain) { _memoryDomains = core; _domain = domain; _watchList.ForEach(w => { if (w.Domain != null) { w.Domain = _memoryDomains[w.Domain.Name]; } }); } [Obsolete("Use domain from individual watch instead")] public MemoryDomain Domain { get { return _domain; } set { _domain = value; } } [Obsolete("Use count property instead", true)] public int ItemCount { get { return Count; } } #region File handling logic - probably needs to be its own class public bool Load(string path, bool append) { var result = LoadFile(path, append); if (result) { if (append) { Changes = true; } else { CurrentFileName = path; Changes = false; } } return result; } public void Reload() { if (!string.IsNullOrWhiteSpace(CurrentFileName)) { LoadFile(CurrentFileName, append: false); Changes = false; } } public bool Save() { if (string.IsNullOrWhiteSpace(CurrentFileName)) { return false; } using (var sw = new StreamWriter(CurrentFileName)) { var sb = new StringBuilder(); sb .Append("Domain ").AppendLine(_domain.Name) .Append("SystemID ").AppendLine(_systemid); foreach (var watch in _watchList) { sb.AppendLine(watch.ToString()); } sw.WriteLine(sb.ToString()); } Changes = false; return true; } public bool SaveAs(FileInfo file) { if (file != null) { CurrentFileName = file.FullName; return Save(); } return false; } private bool LoadFile(string path, bool append) { var domain = string.Empty; var file = new FileInfo(path); if (file.Exists == false) { return false; } var isBizHawkWatch = true; // Hack to support .wch files from other emulators var isOldBizHawkWatch = false; using (var sr = file.OpenText()) { string line; if (!append) { Clear(); } while ((line = sr.ReadLine()) != null) { // .wch files from other emulators start with a number representing the number of watch, that line can be discarded here // Any properly formatted line couldn't possibly be this short anyway, this also takes care of any garbage lines that might be in a file if (line.Length < 5) { isBizHawkWatch = false; continue; } if (line.Length >= 6 && line.Substring(0, 6) == "Domain") { domain = line.Substring(7, line.Length - 7); isBizHawkWatch = true; } if (line.Length >= 8 && line.Substring(0, 8) == "SystemID") { continue; } var numColumns = line.HowMany('\t'); int startIndex; if (numColumns == 5) { // If 5, then this is a post 1.0.5 .wch file if (isBizHawkWatch) { // Do nothing here } else { startIndex = line.IndexOf('\t') + 1; line = line.Substring(startIndex, line.Length - startIndex); // 5 digit value representing the watch position number } } else if (numColumns == 4) { isOldBizHawkWatch = true; // This supports the legacy .wch format from 1.0.5 and earlier } else { continue; // If not 4, something is wrong with this line, ignore it } // Temporary, rename if kept int addr; var memDomain = _memoryDomains.MainMemory; var temp = line.Substring(0, line.IndexOf('\t')); try { addr = int.Parse(temp, NumberStyles.HexNumber); } catch { continue; } startIndex = line.IndexOf('\t') + 1; line = line.Substring(startIndex, line.Length - startIndex); // Type var size = Watch.SizeFromChar(line[0]); startIndex = line.IndexOf('\t') + 1; line = line.Substring(startIndex, line.Length - startIndex); // Signed var type = Watch.DisplayTypeFromChar(line[0]); startIndex = line.IndexOf('\t') + 1; line = line.Substring(startIndex, line.Length - startIndex); // Endian try { startIndex = short.Parse(line[0].ToString()); } catch { continue; } var bigEndian = startIndex != 0; if (isBizHawkWatch && !isOldBizHawkWatch) { startIndex = line.IndexOf('\t') + 1; line = line.Substring(startIndex, line.Length - startIndex); // Domain temp = line.Substring(0, line.IndexOf('\t')); memDomain = _memoryDomains[temp] ?? _memoryDomains.MainMemory; } startIndex = line.IndexOf('\t') + 1; var notes = line.Substring(startIndex, line.Length - startIndex); _watchList.Add( Watch.GenerateWatch( memDomain, addr, size, type, bigEndian, notes)); _domain = _memoryDomains[domain]; } Domain = _memoryDomains[domain] ?? _memoryDomains.MainMemory; _currentFilename = path; } if (!append) { Changes = false; } else { Changes = true; } return true; } #endregion } }