1 using System;
2 using System.Collections.Generic;
3 using System.ComponentModel;
4 using System.Drawing;
5 using System.Data;
6 using System.Linq;
7 using System.Text;
8 using System.Threading.Tasks;
9 using System.Windows.Forms;
10 using System.Runtime.InteropServices;
11 using Mesen.GUI.Controls;
12 using System.Drawing.Imaging;
13 using Mesen.GUI.Config;
14 using Mesen.GUI.Forms;
15 using System.Drawing.Drawing2D;
16 
17 namespace Mesen.GUI.Debugger.Controls
18 {
19 	public partial class ctrlSpriteViewer : BaseControl, ICompactControl
20 	{
SelectTilePaletteHandler(int tileIndex, int paletteIndex)21 		public delegate void SelectTilePaletteHandler(int tileIndex, int paletteIndex);
22 		public event SelectTilePaletteHandler OnSelectTilePalette;
23 		private byte[] _spriteRam;
24 		private byte[] _spritePixelData;
25 
26 		private int _selectedSprite = -1;
27 		private SpriteInfo _spriteInfo = new SpriteInfo();
28 
29 		private bool _largeSprites = false;
30 		private bool _prevLargeSprites = true;
31 		private int _spritePatternAddr;
32 		private bool _forceRefresh;
33 		private Point? _previewMousePosition = null;
34 		private int _contextMenuSpriteIndex = -1;
35 		private bool _copyPreview = false;
36 		private Bitmap _imgSprites;
37 		private Bitmap _scaledSprites = new Bitmap(256, 512, PixelFormat.Format32bppPArgb);
38 		private Bitmap _screenPreview = new Bitmap(256, 240, PixelFormat.Format32bppPArgb);
39 		private HdPackCopyHelper _hdCopyHelper = new HdPackCopyHelper();
40 		private bool _firstDraw = true;
41 		private int _originalSpriteHeight = 0;
42 		private int _originalTileHeight = 0;
43 		private Size _originalPreviewSize;
44 		private double _scale = 1;
45 
ctrlSpriteViewer()46 		public ctrlSpriteViewer()
47 		{
48 			InitializeComponent();
49 
50 			if(!IsDesignMode) {
51 				picPreview.Image = new Bitmap(256, 240, PixelFormat.Format32bppPArgb);
52 				picSprites.Image = new Bitmap(256, 512, PixelFormat.Format32bppPArgb);
53 
54 				chkDisplaySpriteOutlines.Checked = ConfigManager.Config.DebugInfo.SpriteViewerDisplaySpriteOutlines;
55 
56 				_originalSpriteHeight = picSprites.Height;
57 				_originalTileHeight = picTile.Height;
58 				_originalPreviewSize = picPreview.Size;
59 			}
60 		}
61 
GetCompactSize(bool includeMargins)62 		public Size GetCompactSize(bool includeMargins)
63 		{
64 			return new Size(picSprites.Width, _prevLargeSprites ? picSprites.Height : (picSprites.Height * 2));
65 		}
66 
ScaleImage(double scale)67 		public void ScaleImage(double scale)
68 		{
69 			_scale *= scale;
70 
71 			picSprites.Size = new Size((int)(picSprites.Width * scale), (int)(picSprites.Height * scale));
72 			if(_largeSprites) {
73 				picPreview.Size = _originalPreviewSize;
74 			} else {
75 				picPreview.Size = new Size((int)(_originalPreviewSize.Width * _scale), (int)(_originalPreviewSize.Height * _scale));
76 			}
77 			_originalSpriteHeight = (int)(_originalSpriteHeight * scale);
78 
79 			picSprites.InterpolationMode = scale > 1 ? InterpolationMode.NearestNeighbor : InterpolationMode.Default;
80 			picPreview.InterpolationMode = scale > 1 ? InterpolationMode.NearestNeighbor : InterpolationMode.Default;
81 		}
82 
ProcessCmdKey(ref Message msg, Keys keyData)83 		protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
84 		{
85 			if(ctxMenu.ProcessCommandKey(ref msg, keyData)) {
86 				return true;
87 			}
88 			return base.ProcessCmdKey(ref msg, keyData);
89 		}
90 
OnLoad(EventArgs e)91 		protected override void OnLoad(EventArgs e)
92 		{
93 			base.OnLoad(e);
94 			if(!IsDesignMode) {
95 				mnuCopyToClipboard.InitShortcut(this, nameof(DebuggerShortcutsConfig.Copy));
96 				mnuEditInMemoryViewer.InitShortcut(this, nameof(DebuggerShortcutsConfig.CodeWindow_EditInMemoryViewer));
97 			}
98 		}
99 
GetData()100 		public void GetData()
101 		{
102 			DebugState state = new DebugState();
103 			InteropEmu.DebugGetState(ref state);
104 			_largeSprites = state.PPU.ControlFlags.LargeSprites != 0;
105 			_spritePatternAddr = state.PPU.ControlFlags.SpritePatternAddr;
106 
107 			_spriteRam = InteropEmu.DebugGetMemoryState(DebugMemoryType.SpriteMemory);
108 			_spritePixelData = InteropEmu.DebugGetSprites();
109 
110 			_hdCopyHelper.RefreshData();
111 		}
112 
RefreshViewer()113 		public void RefreshViewer()
114 		{
115 			_forceRefresh = true;
116 
117 			GCHandle handle = GCHandle.Alloc(_spritePixelData, GCHandleType.Pinned);
118 			try {
119 				Bitmap source = new Bitmap(64, 128, 4*64, PixelFormat.Format32bppPArgb, handle.AddrOfPinnedObject());
120 
121 				Bitmap sprites = new Bitmap(64, 128, PixelFormat.Format32bppPArgb);
122 				using(Graphics g = Graphics.FromImage(sprites)) {
123 					if(_largeSprites) {
124 						g.DrawImage(source, 0, 0);
125 					} else {
126 						for(int i = 0; i < 8; i++) {
127 							g.DrawImage(source, 0, 8 * i, new RectangleF(0, 8 * i * 2, 64, 8), GraphicsUnit.Pixel);
128 						}
129 					}
130 				}
131 				_imgSprites = sprites;
132 
133 				using(Graphics g = Graphics.FromImage(_scaledSprites)) {
134 					g.Clear(Color.FromArgb(64, 64, 64));
135 					g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
136 					g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;
137 					g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half;
138 
139 					g.ScaleTransform(4, 4);
140 					g.DrawImageUnscaled(sprites, 0, 0);
141 				}
142 			} finally {
143 				handle.Free();
144 			}
145 
146 			if(_prevLargeSprites != _largeSprites) {
147 				ToggleSpriteMode();
148 				_prevLargeSprites = _largeSprites;
149 			}
150 
151 			if(_previewMousePosition.HasValue) {
152 				SelectSpriteUnderCursor();
153 			}
154 			CreateScreenPreview();
155 			picPreview.Refresh();
156 
157 			if(_firstDraw) {
158 				//Update the UI with the first sprite when showing for the first time
159 				UpdateTileInfo(0);
160 				_selectedSprite = -1;
161 				_firstDraw = false;
162 			}
163 
164 			DrawHud();
165 		}
166 
ToggleSpriteMode()167 		private void ToggleSpriteMode()
168 		{
169 			if(_largeSprites) {
170 				picSprites.Image = new Bitmap(256, 512, PixelFormat.Format32bppPArgb);
171 				picSprites.Height = _originalSpriteHeight;
172 				picTile.Height = _originalTileHeight;
173 				picPreview.Size = _originalPreviewSize;
174 
175 				tlpMain.SetRowSpan(picSprites, 2);
176 				tlpMain.SetColumnSpan(picPreview, 4);
177 				tlpInfo.Controls.Add(picPreview, 1, 5);
178 				lblScreenPreview.Visible = true;
179 			} else {
180 				picSprites.Image = new Bitmap(256, 256, PixelFormat.Format32bppPArgb);
181 				picSprites.Height = (_originalSpriteHeight - 2) / 2 + 2;
182 				picTile.Height = (_originalTileHeight - 2) / 2 + 2;
183 				picPreview.Size = new Size((int)(_originalPreviewSize.Width * _scale), (int)(_originalPreviewSize.Height * _scale));
184 
185 				tlpMain.SetRowSpan(picSprites, 1);
186 				tlpMain.SetColumnSpan(picPreview, 1);
187 				tlpMain.Controls.Add(picPreview, 0, 1);
188 				lblScreenPreview.Visible = false;
189 			}
190 		}
191 
DrawHud()192 		private void DrawHud()
193 		{
194 			using(Graphics g = Graphics.FromImage(picSprites.Image)) {
195 				g.DrawImage(_scaledSprites, 0, 0);
196 
197 				if(_selectedSprite >= 0) {
198 					int x = _selectedSprite % 8;
199 					int y = _selectedSprite / 8;
200 					int spriteHeight = _largeSprites ? 64 : 32;
201 					using(SolidBrush brush = new SolidBrush(Color.FromArgb(100, 255, 255, 255))) {
202 						g.FillRectangle(brush, x * 32, y * spriteHeight, 32, spriteHeight);
203 					}
204 					g.DrawRectangle(Pens.White, x * 32, y * spriteHeight, 31, spriteHeight - 1);
205 
206 					if(ConfigManager.Config.DebugInfo.PpuShowInformationOverlay) {
207 						string tooltipText = (
208 							"Sprite:    $" + _spriteInfo.SpriteIndex.ToString("X2") + Environment.NewLine +
209 							"Tile:      $" + _spriteInfo.TileIndex.ToString("X2") + Environment.NewLine +
210 							"Position:  " + _spriteInfo.SpriteX.ToString() + ", " + _spriteInfo.SpriteY.ToString() + Environment.NewLine +
211 							"Flags:     " + (_spriteInfo.HorizontalMirror ? "H" : "-") + (_spriteInfo.VerticalMirror ? "V" : "-") + (_spriteInfo.BackgroundPriority ? "B" : "-") + Environment.NewLine +
212 							"Palette:   " + _spriteInfo.PaletteIndex.ToString() + " ($" + (0x3F10 + (_spriteInfo.PaletteIndex << 2)).ToString("X4") + ")" + Environment.NewLine
213 						);
214 
215 						PpuViewerHelper.DrawOverlayTooltip(picSprites.Image, tooltipText, picTile.Image, _spriteInfo.PaletteIndex + 4, _selectedSprite >= 32, g);
216 					}
217 				}
218 			}
219 
220 			picSprites.Refresh();
221 		}
222 
CreateScreenPreview()223 		private void CreateScreenPreview()
224 		{
225 			GCHandle handle = GCHandle.Alloc(_spritePixelData, GCHandleType.Pinned);
226 			try {
227 				Bitmap source = new Bitmap(64, 128, 4*64, PixelFormat.Format32bppPArgb, handle.AddrOfPinnedObject());
228 
229 				using(Graphics g = Graphics.FromImage(_screenPreview)) {
230 					g.InterpolationMode = InterpolationMode.NearestNeighbor;
231 					g.SmoothingMode = SmoothingMode.None;
232 					g.PixelOffsetMode = PixelOffsetMode.Half;
233 					g.Clear(Color.Transparent);
234 
235 					for(int i = 63; i >= 0; i--) {
236 						if(i != _selectedSprite) {
237 							DrawSprite(source, g, i);
238 						}
239 					}
240 
241 					if(ConfigManager.Config.DebugInfo.SpriteViewerDisplaySpriteOutlines) {
242 						using(Pen pen = new Pen(Color.White, 1)) {
243 							for(int i = 63; i >= 0; i--) {
244 								int spriteY = _spriteRam[i * 4];
245 								int spriteX = _spriteRam[i * 4 + 3];
246 								g.DrawRectangle(pen, new Rectangle(spriteX, spriteY, 9, _largeSprites ? 17 : 9));
247 							}
248 						}
249 					}
250 
251 					if(_selectedSprite >= 0) {
252 						DrawSprite(source, g, _selectedSprite);
253 					}
254 				}
255 
256 				using(Graphics g = Graphics.FromImage(picPreview.Image)) {
257 					g.Clear(Color.FromArgb(64, 64, 64));
258 					g.DrawImage(_screenPreview, 0, 0);
259 				}
260 				picPreview.Invalidate();
261 			} finally {
262 				handle.Free();
263 			}
264 		}
265 
DrawSprite(Bitmap source, Graphics g, int i)266 		private void DrawSprite(Bitmap source, Graphics g, int i)
267 		{
268 			int spriteY = _spriteRam[i*4];
269 			int spriteX = _spriteRam[i*4+3];
270 
271 			if(spriteY < 240) {
272 				g.DrawImage(source, new Rectangle(spriteX, spriteY, 8, _largeSprites ? 16 : 8), new Rectangle((i % 8) * 8, (i / 8) * 16, 8, _largeSprites ? 16 : 8), GraphicsUnit.Pixel);
273 			}
274 
275 			if(_selectedSprite == i) {
276 				using(Pen pen = new Pen(Color.Red, 2)) {
277 					g.DrawRectangle(pen, new Rectangle(spriteX - 1, spriteY - 1, 10, _largeSprites ? 18 : 10));
278 				}
279 			}
280 		}
281 
picSprites_MouseMove(object sender, MouseEventArgs e)282 		private void picSprites_MouseMove(object sender, MouseEventArgs e)
283 		{
284 			_previewMousePosition = null;
285 
286 			int tileX = Math.Max(0, Math.Min(e.X * 256 / (picSprites.Width - 2) / 32, 31));
287 			int tileY = 0;
288 			int ramAddr = 0;
289 			if(_largeSprites) {
290 				tileY = Math.Max(0, Math.Min(e.Y * 512 / (picSprites.Height - 2) / 64, 63));
291 				ramAddr = ((tileY << 3) + tileX) << 2;
292 			} else {
293 				tileY = Math.Max(0, Math.Min(e.Y * 256 / (picSprites.Height - 2) / 32, 31));
294 				ramAddr = ((tileY << 3) + tileX) << 2;
295 			}
296 
297 			ramAddr = Math.Min(ramAddr, 63 * 4);
298 
299 			if(ramAddr / 4 == _selectedSprite && !_forceRefresh) {
300 				return;
301 			}
302 
303 			UpdateTileInfo(ramAddr);
304 			DrawHud();
305 		}
306 
UpdateTileInfo(int ramAddr)307 		private void UpdateTileInfo(int ramAddr)
308 		{
309 			_forceRefresh = false;
310 			_selectedSprite = ramAddr / 4;
311 
312 			int spriteY = _spriteRam[ramAddr];
313 			int tileIndex = _spriteRam[ramAddr + 1];
314 			int attributes = _spriteRam[ramAddr + 2];
315 			int spriteX = _spriteRam[ramAddr + 3];
316 
317 			int tileAddr;
318 			if(_largeSprites) {
319 				tileAddr = ((tileIndex & 0x01) == 0x01 ? 0x1000 : 0x0000) + ((tileIndex & 0xFE) << 4);
320 			} else {
321 				tileAddr = _spritePatternAddr + (tileIndex << 4);
322 			}
323 
324 			this.ctrlTilePalette.SelectedPalette = (attributes & 0x03) + 4;
325 
326 			int paletteAddr = 0x3F10 + ((attributes & 0x03) << 2);
327 			bool verticalMirror = (attributes & 0x80) == 0x80;
328 			bool horizontalMirror = (attributes & 0x40) == 0x40;
329 			bool backgroundPriority = (attributes & 0x20) == 0x20;
330 
331 			_spriteInfo = new SpriteInfo() {
332 				SpriteIndex = _selectedSprite,
333 				SpriteX = spriteX,
334 				SpriteY = spriteY,
335 				TileIndex = tileIndex,
336 				HorizontalMirror = horizontalMirror,
337 				VerticalMirror = verticalMirror,
338 				BackgroundPriority = backgroundPriority,
339 				PaletteIndex = (attributes & 0x03)
340 			};
341 
342 			this.txtSpriteIndex.Text = _selectedSprite.ToString("X2");
343 			this.txtTileIndex.Text = tileIndex.ToString("X2");
344 			this.txtTileAddress.Text = tileAddr.ToString("X4");
345 			this.txtPosition.Text = spriteX.ToString() + ", " + spriteY.ToString();
346 			this.txtPaletteAddress.Text = paletteAddr.ToString("X4");
347 			this.chkVerticalMirroring.Checked = verticalMirror;
348 			this.chkHorizontalMirroring.Checked = horizontalMirror;
349 			this.chkBackgroundPriority.Checked = backgroundPriority;
350 
351 			int tileX = _selectedSprite % 8;
352 			int tileY = _selectedSprite / 8;
353 
354 			int spriteHeight = _largeSprites ? 64 : 32;
355 			picTile.Image = PpuViewerHelper.GetPreview(new Point(tileX * 32, tileY * spriteHeight), new Size(32, spriteHeight), 2, _scaledSprites);
356 
357 			this.CreateScreenPreview();
358 		}
359 
picSprites_MouseLeave(object sender, EventArgs e)360 		private void picSprites_MouseLeave(object sender, EventArgs e)
361 		{
362 			this._selectedSprite = -1;
363 			this.CreateScreenPreview();
364 			this.DrawHud();
365 		}
366 
367 		string _copyData;
mnuCopyHdPack_Click(object sender, EventArgs e)368 		private void mnuCopyHdPack_Click(object sender, EventArgs e)
369 		{
370 			Clipboard.SetText(_copyData);
371 		}
372 
ctxMenu_Opening(object sender, CancelEventArgs e)373 		private void ctxMenu_Opening(object sender, CancelEventArgs e)
374 		{
375 			mnuCopyAllSpritesHdPack.Visible = Control.ModifierKeys == Keys.Shift;
376 
377 			if(_selectedSprite < 0) {
378 				_contextMenuSpriteIndex = -1;
379 				return;
380 			}
381 
382 			_contextMenuSpriteIndex = _selectedSprite;
383 			_copyData = ToHdPackFormat(_selectedSprite);
384 		}
385 
ToHdPackFormat(int spriteIndex)386 		private string ToHdPackFormat(int spriteIndex)
387 		{
388 			int ramAddr = spriteIndex * 4;
389 			int tileIndex = _spriteRam[ramAddr + 1];
390 			int palette = (_spriteRam[ramAddr + 2] & 0x03) + 4;
391 
392 			int tileAddr;
393 			if(_largeSprites) {
394 				tileAddr = ((tileIndex & 0x01) == 0x01 ? 0x1000 : 0x0000) + ((tileIndex & 0xFE) << 4);
395 			} else {
396 				tileAddr = _spritePatternAddr + (tileIndex << 4);
397 			}
398 
399 			return _hdCopyHelper.ToHdPackFormat(tileAddr, palette, true, false);
400 		}
401 
picPreview_MouseMove(object sender, MouseEventArgs e)402 		private void picPreview_MouseMove(object sender, MouseEventArgs e)
403 		{
404 			_previewMousePosition = e.Location;
405 			SelectSpriteUnderCursor();
406 			DrawHud();
407 		}
408 
SelectSpriteUnderCursor()409 		private void SelectSpriteUnderCursor()
410 		{
411 			Point p = _previewMousePosition.Value;
412 			int xPos = p.X * 256 / (picPreview.Width - 2);
413 			int yPos = p.Y * 240 / (picPreview.Height - 2);
414 			int prevSprite = _selectedSprite;
415 			_selectedSprite = -1;
416 			for(int i = 0x100 - 4; i >= 0; i-=4) {
417 				int spriteY = _spriteRam[i];
418 				int spriteX = _spriteRam[i + 3];
419 
420 				if(xPos >= spriteX && xPos < spriteX + 8 && yPos >= spriteY && yPos < spriteY + (_largeSprites ? 16 : 8)) {
421 					_selectedSprite = i / 4;
422 					break;
423 				}
424 			}
425 
426 			if(prevSprite != _selectedSprite) {
427 				if(_selectedSprite >= 0) {
428 					UpdateTileInfo(_selectedSprite * 4);
429 				}
430 				CreateScreenPreview();
431 			}
432 		}
433 
picPreview_MouseLeave(object sender, EventArgs e)434 		private void picPreview_MouseLeave(object sender, EventArgs e)
435 		{
436 			CreateScreenPreview();
437 		}
438 
ShowInChrViewer()439 		private void ShowInChrViewer()
440 		{
441 			if(_selectedSprite < 0 && _contextMenuSpriteIndex < 0) {
442 				return;
443 			}
444 
445 			int ramAddr = (_selectedSprite >= 0 ? _selectedSprite : _contextMenuSpriteIndex) * 4;
446 			int tileIndex = _spriteRam[ramAddr + 1];
447 			int palette = (_spriteRam[ramAddr + 2] & 0x03) + 4;
448 
449 			DebugState state = new DebugState();
450 			InteropEmu.DebugGetState(ref state);
451 
452 			if(_largeSprites) {
453 				if(tileIndex % 2 == 1) {
454 					tileIndex += 256;
455 					tileIndex--;
456 				}
457 				OnSelectTilePalette?.Invoke(tileIndex, palette);
458 			} else {
459 				int tileIndexOffset = state.PPU.ControlFlags.SpritePatternAddr == 0x1000 ? 256 : 0;
460 				OnSelectTilePalette?.Invoke(tileIndex+tileIndexOffset, palette);
461 			}
462 		}
463 
picSprites_DoubleClick(object sender, EventArgs e)464 		private void picSprites_DoubleClick(object sender, EventArgs e)
465 		{
466 			ShowInChrViewer();
467 		}
468 
mnuShowInChrViewer_Click(object sender, EventArgs e)469 		private void mnuShowInChrViewer_Click(object sender, EventArgs e)
470 		{
471 			ShowInChrViewer();
472 		}
473 
picSprites_MouseEnter(object sender, EventArgs e)474 		private void picSprites_MouseEnter(object sender, EventArgs e)
475 		{
476 			_copyPreview = false;
477 			if(this.ParentForm.ContainsFocus) {
478 				this.Focus();
479 			}
480 		}
481 
picPreview_MouseEnter(object sender, EventArgs e)482 		private void picPreview_MouseEnter(object sender, EventArgs e)
483 		{
484 			_copyPreview = true;
485 			if(this.ParentForm.ContainsFocus) {
486 				this.Focus();
487 			}
488 		}
489 
mnuCopyToClipboard_Click(object sender, EventArgs e)490 		private void mnuCopyToClipboard_Click(object sender, EventArgs e)
491 		{
492 			CopyToClipboard();
493 		}
494 
GetCopyBitmap()495 		private Bitmap GetCopyBitmap()
496 		{
497 			Bitmap src = _copyPreview ? _screenPreview : _imgSprites;
498 			Bitmap target = new Bitmap(src.Width, src.Height);
499 			using(Graphics g = Graphics.FromImage(target)) {
500 				g.Clear(Color.FromArgb(64, 64, 64));
501 				g.DrawImage(src, 0, 0);
502 			}
503 			return target;
504 		}
505 
CopyToClipboard()506 		public void CopyToClipboard()
507 		{
508 			using(Bitmap target = GetCopyBitmap()) {
509 				Clipboard.SetImage(target);
510 			}
511 		}
512 
mnuExportToPng_Click(object sender, EventArgs e)513 		private void mnuExportToPng_Click(object sender, EventArgs e)
514 		{
515 			using(SaveFileDialog sfd = new SaveFileDialog()) {
516 				sfd.SetFilter("PNG files|*.png");
517 				if(sfd.ShowDialog() == DialogResult.OK) {
518 					using(Bitmap target = GetCopyBitmap()) {
519 						target.Save(sfd.FileName, System.Drawing.Imaging.ImageFormat.Png);
520 					}
521 				}
522 			}
523 		}
524 
mnuCopyAllSpritesHdPack_Click(object sender, EventArgs e)525 		private void mnuCopyAllSpritesHdPack_Click(object sender, EventArgs e)
526 		{
527 			StringBuilder sb = new StringBuilder();
528 			for(int i = 0; i < 64; i++) {
529 				int ramAddr = i * 4;
530 				int spriteY = _spriteRam[ramAddr];
531 				int spriteX = _spriteRam[ramAddr+3];
532 				int attributes = _spriteRam[ramAddr+2];
533 				bool horizontalMirror = (attributes & 0x40) == 0x40;
534 				bool verticalMirror = (attributes & 0x80) == 0x80;
535 
536 				if(spriteY >= 0 && spriteY < 240) {
537 					sb.AppendLine(
538 						ToHdPackFormat(i) + "," +
539 						spriteX.ToString() + "," +
540 						spriteY.ToString() + "," +
541 						(horizontalMirror ? "Y" : "N") + "," +
542 						(verticalMirror ? "Y" : "N")
543 					);
544 				}
545 			}
546 			if(sb.Length > 0) {
547 				Clipboard.SetText(sb.ToString());
548 			} else {
549 				Clipboard.Clear();
550 			}
551 		}
552 
mnuEditInMemoryViewer_Click(object sender, EventArgs e)553 		private void mnuEditInMemoryViewer_Click(object sender, EventArgs e)
554 		{
555 			if(_selectedSprite < 0 && _contextMenuSpriteIndex < 0) {
556 				return;
557 			}
558 
559 			int ramAddr = (_selectedSprite >= 0 ? _selectedSprite : _contextMenuSpriteIndex) * 4;
560 			int tileIndex = _spriteRam[ramAddr + 1];
561 
562 			DebugState state = new DebugState();
563 			InteropEmu.DebugGetState(ref state);
564 
565 			int tileIndexOffset = (!_largeSprites && state.PPU.ControlFlags.SpritePatternAddr == 0x1000) ? 256 : 0;
566 			DebugWindowManager.OpenMemoryViewer((tileIndex + tileIndexOffset) * 16, DebugMemoryType.PpuMemory);
567 		}
568 
chkDisplaySpriteOutlines_Click(object sender, EventArgs e)569 		private void chkDisplaySpriteOutlines_Click(object sender, EventArgs e)
570 		{
571 			ConfigManager.Config.DebugInfo.SpriteViewerDisplaySpriteOutlines = chkDisplaySpriteOutlines.Checked;
572 			ConfigManager.ApplyChanges();
573 			RefreshViewer();
574 		}
575 
576 		private class SpriteInfo
577 		{
578 			public int SpriteIndex;
579 			public int TileIndex;
580 			public bool HorizontalMirror;
581 			public bool VerticalMirror;
582 			public bool BackgroundPriority;
583 			public int SpriteX;
584 			public int SpriteY;
585 			public int PaletteIndex;
586 		}
587 	}
588 }
589