2014-04-19 19:01:13 +00:00
using System ;
using System.Collections.Generic ;
using System.ComponentModel ;
using System.Data ;
using System.Drawing ;
using System.Linq ;
using System.Text ;
using System.Windows.Forms ;
using BizHawk.Client.Common ;
using BizHawk.Emulation.Cores.Atari.Atari2600 ;
2014-12-05 00:05:40 +00:00
using BizHawk.Emulation.Common ;
2014-04-19 19:01:13 +00:00
namespace BizHawk.Client.EmuHawk
{
public partial class Atari2600Debugger : Form , IToolForm
{
2014-04-21 19:59:21 +00:00
// TODO:
// Take control of mainform
// Consider how to handle trace logger (the two will compete with each other with the TakeContents() method)
2014-05-31 21:57:28 +00:00
// Step Over
2014-04-21 19:59:21 +00:00
// Step Out
2014-05-31 21:57:28 +00:00
// Breakpoints - Double click toggle
2014-05-26 18:23:58 +00:00
// Save breakpoints to file?
2014-05-27 20:07:03 +00:00
// Video Frame advance
// Add to toolbox
2014-04-19 19:01:13 +00:00
private Atari2600 _core = Global . Emulator as Atari2600 ;
2014-04-20 01:19:33 +00:00
private readonly List < string > _instructions = new List < string > ( ) ;
2014-04-19 19:01:13 +00:00
2014-05-26 18:23:58 +00:00
private readonly AtariBreakpointList Breakpoints = new AtariBreakpointList ( ) ;
2014-04-20 16:19:08 +00:00
private int _defaultWidth ;
private int _defaultHeight ;
2014-05-31 21:57:28 +00:00
private bool _programmaticUpdateOfRegisterBoxes = false ; // Winforms have no way to programmitcally set the value of a widget without invoking the change event so hacks like this are necessary
2014-04-21 19:59:21 +00:00
//the opsize table is used to quickly grab the instruction sizes (in bytes)
private readonly byte [ ] opsize = new byte [ ]
{
/*0x00*/ 1 , 2 , 0 , 0 , 0 , 2 , 2 , 0 , 1 , 2 , 1 , 0 , 0 , 3 , 3 , 0 ,
/*0x10*/ 2 , 2 , 0 , 0 , 0 , 2 , 2 , 0 , 1 , 3 , 0 , 0 , 0 , 3 , 3 , 0 ,
/*0x20*/ 3 , 2 , 0 , 0 , 2 , 2 , 2 , 0 , 1 , 2 , 1 , 0 , 3 , 3 , 3 , 0 ,
/*0x30*/ 2 , 2 , 0 , 0 , 0 , 2 , 2 , 0 , 1 , 3 , 0 , 0 , 0 , 3 , 3 , 0 ,
/*0x40*/ 1 , 2 , 0 , 0 , 0 , 2 , 2 , 0 , 1 , 2 , 1 , 0 , 3 , 3 , 3 , 0 ,
/*0x50*/ 2 , 2 , 0 , 0 , 0 , 2 , 2 , 0 , 1 , 3 , 0 , 0 , 0 , 3 , 3 , 0 ,
/*0x60*/ 1 , 2 , 0 , 0 , 0 , 2 , 2 , 0 , 1 , 2 , 1 , 0 , 3 , 3 , 3 , 0 ,
/*0x70*/ 2 , 2 , 0 , 0 , 0 , 2 , 2 , 0 , 1 , 3 , 0 , 0 , 0 , 3 , 3 , 0 ,
/*0x80*/ 0 , 2 , 0 , 0 , 2 , 2 , 2 , 0 , 1 , 0 , 1 , 0 , 3 , 3 , 3 , 0 ,
/*0x90*/ 2 , 2 , 0 , 0 , 2 , 2 , 2 , 0 , 1 , 3 , 1 , 0 , 0 , 3 , 0 , 0 ,
/*0xA0*/ 2 , 2 , 2 , 0 , 2 , 2 , 2 , 0 , 1 , 2 , 1 , 0 , 3 , 3 , 3 , 0 ,
/*0xB0*/ 2 , 2 , 0 , 0 , 2 , 2 , 2 , 0 , 1 , 3 , 1 , 0 , 3 , 3 , 3 , 0 ,
/*0xC0*/ 2 , 2 , 0 , 0 , 2 , 2 , 2 , 0 , 1 , 2 , 1 , 0 , 3 , 3 , 3 , 0 ,
/*0xD0*/ 2 , 2 , 0 , 0 , 0 , 2 , 2 , 0 , 1 , 3 , 0 , 0 , 0 , 3 , 3 , 0 ,
/*0xE0*/ 2 , 2 , 0 , 0 , 2 , 2 , 2 , 0 , 1 , 2 , 1 , 0 , 3 , 3 , 3 , 0 ,
/*0xF0*/ 2 , 2 , 0 , 0 , 0 , 2 , 2 , 0 , 1 , 3 , 0 , 0 , 0 , 3 , 3 , 0
} ;
/ * the optype table is a quick way to grab the addressing mode for any 6502 opcode
/ /
// 0 = Implied\Accumulator\Immediate\Branch\NULL
// 1 = (Indirect,X)
// 2 = Zero Page
// 3 = Absolute
// 4 = (Indirect),Y
// 5 = Zero Page,X
// 6 = Absolute,Y
// 7 = Absolute,X
// 8 = Zero Page,Y
* /
private readonly byte [ ] optype = new byte [ ]
{
/*0x00*/ 0 , 1 , 0 , 0 , 0 , 2 , 2 , 0 , 0 , 0 , 0 , 0 , 0 , 3 , 3 , 0 ,
/*0x10*/ 0 , 4 , 0 , 0 , 0 , 5 , 5 , 0 , 0 , 6 , 0 , 0 , 0 , 7 , 7 , 0 ,
/*0x20*/ 0 , 1 , 0 , 0 , 2 , 2 , 2 , 0 , 0 , 0 , 0 , 0 , 3 , 3 , 3 , 0 ,
/*0x30*/ 0 , 4 , 0 , 0 , 0 , 5 , 5 , 0 , 0 , 6 , 0 , 0 , 0 , 7 , 7 , 0 ,
/*0x40*/ 0 , 1 , 0 , 0 , 0 , 2 , 2 , 0 , 0 , 0 , 0 , 0 , 0 , 3 , 3 , 0 ,
/*0x50*/ 0 , 4 , 0 , 0 , 0 , 5 , 5 , 0 , 0 , 6 , 0 , 0 , 0 , 7 , 7 , 0 ,
/*0x60*/ 0 , 1 , 0 , 0 , 0 , 2 , 2 , 0 , 0 , 0 , 0 , 0 , 3 , 3 , 3 , 0 ,
/*0x70*/ 0 , 4 , 0 , 0 , 0 , 5 , 5 , 0 , 0 , 6 , 0 , 0 , 0 , 7 , 7 , 0 ,
/*0x80*/ 0 , 1 , 0 , 0 , 2 , 2 , 2 , 0 , 0 , 0 , 0 , 0 , 3 , 3 , 3 , 0 ,
/*0x90*/ 0 , 4 , 0 , 0 , 5 , 5 , 8 , 0 , 0 , 6 , 0 , 0 , 0 , 7 , 0 , 0 ,
/*0xA0*/ 0 , 1 , 0 , 0 , 2 , 2 , 2 , 0 , 0 , 0 , 0 , 0 , 3 , 3 , 3 , 0 ,
/*0xB0*/ 0 , 4 , 0 , 0 , 5 , 5 , 8 , 0 , 0 , 6 , 0 , 0 , 7 , 7 , 6 , 0 ,
/*0xC0*/ 0 , 1 , 0 , 0 , 2 , 2 , 2 , 0 , 0 , 0 , 0 , 0 , 3 , 3 , 3 , 0 ,
/*0xD0*/ 0 , 4 , 0 , 0 , 0 , 5 , 5 , 0 , 0 , 6 , 0 , 0 , 0 , 7 , 7 , 0 ,
/*0xE0*/ 0 , 1 , 0 , 0 , 2 , 2 , 2 , 0 , 0 , 0 , 0 , 0 , 3 , 3 , 3 , 0 ,
/*0xF0*/ 0 , 4 , 0 , 0 , 0 , 5 , 5 , 0 , 0 , 6 , 0 , 0 , 0 , 7 , 7 , 0
} ;
2014-04-19 19:01:13 +00:00
public Atari2600Debugger ( )
{
InitializeComponent ( ) ;
2014-04-20 01:19:33 +00:00
TraceView . QueryItemText + = TraceView_QueryItemText ;
TraceView . VirtualMode = true ;
2014-05-26 18:23:58 +00:00
BreakpointView . QueryItemText + = BreakPointView_QueryItemText ;
BreakpointView . VirtualMode = true ;
2014-04-20 16:19:08 +00:00
TopMost = Global . Config . Atari2600DebuggerSettings . TopMost ;
2014-04-20 01:19:33 +00:00
2014-04-20 16:19:08 +00:00
Closing + = ( o , e ) = > Shutdown ( ) ;
2014-05-26 18:23:58 +00:00
Breakpoints . Callback = BreakpointCallback ;
2014-04-19 19:01:13 +00:00
}
private void Atari2600Debugger_Load ( object sender , EventArgs e )
{
2014-04-20 16:19:08 +00:00
_defaultWidth = Size . Width ;
_defaultHeight = Size . Height ;
2014-04-19 19:01:13 +00:00
// TODO: some kind of method like PauseAndRelinquishControl() which will set a flag preventing unpausing by the user, and then a ResumeControl() method that is done on close
2014-04-20 16:19:08 +00:00
//GlobalWin.MainForm.PauseEmulator();
2014-12-05 00:05:40 +00:00
( _core as IDebuggable ) . Tracer . Enabled = true ;
2014-04-20 16:19:08 +00:00
if ( Global . Config . Atari2600DebuggerSettings . UseWindowPosition )
{
Location = Global . Config . Atari2600DebuggerSettings . WindowPosition ;
}
if ( Global . Config . Atari2600DebuggerSettings . UseWindowSize )
{
Size = Global . Config . Atari2600DebuggerSettings . WindowSize ;
}
2014-05-30 01:10:10 +00:00
UpdateBreakpointRemoveButton ( ) ;
UpdateValues ( ) ;
}
private IEnumerable < int > SelectedIndices
{
get { return BreakpointView . SelectedIndices . Cast < int > ( ) ; }
}
private IEnumerable < AtariBreakpoint > SelectedItems
{
get { return SelectedIndices . Select ( index = > Breakpoints [ index ] ) ; }
}
private void UpdateBreakpointRemoveButton ( )
{
RemoveBreakpointButton . Enabled = BreakpointView . SelectedIndices . Count > 0 ;
2014-04-20 16:19:08 +00:00
}
private void Shutdown ( )
{
//TODO: add a Mainform.ResumeControl() call
2014-12-05 00:05:40 +00:00
( _core as IDebuggable ) . Tracer . TakeContents ( ) ;
( _core as IDebuggable ) . Tracer . Enabled = false ;
2014-04-19 19:01:13 +00:00
}
public void Restart ( )
{
// TODO
}
2014-08-19 19:24:17 +00:00
public bool AskSaveChanges ( )
2014-04-19 19:01:13 +00:00
{
2014-04-20 16:19:08 +00:00
return true ;
2014-04-19 19:01:13 +00:00
}
public bool UpdateBefore
{
2014-05-26 19:38:02 +00:00
get { return false ; }
2014-04-19 19:01:13 +00:00
}
public void UpdateValues ( )
{
2014-05-31 21:57:28 +00:00
_programmaticUpdateOfRegisterBoxes = true ;
2014-04-20 01:19:33 +00:00
var flags = _core . GetCpuFlagsAndRegisters ( ) ;
PCRegisterBox . Text = flags [ "PC" ] . ToString ( ) ;
2014-04-19 22:23:13 +00:00
2014-04-20 01:19:33 +00:00
SPRegisterBox . Text = flags [ "S" ] . ToString ( ) ;
SPRegisterHexBox . Text = string . Format ( "{0:X2}" , flags [ "S" ] ) ;
SPRegisterBinaryBox . Text = ToBinStr ( flags [ "S" ] ) ;
2014-04-19 22:23:13 +00:00
2014-04-20 01:19:33 +00:00
ARegisterBox . Text = flags [ "A" ] . ToString ( ) ;
ARegisterHexBox . Text = string . Format ( "{0:X2}" , flags [ "A" ] ) ;
ARegisterBinaryBox . Text = ToBinStr ( flags [ "A" ] ) ;
2014-04-19 22:23:13 +00:00
2014-04-20 01:19:33 +00:00
XRegisterBox . Text = flags [ "X" ] . ToString ( ) ;
XRegisterHexBox . Text = string . Format ( "{0:X2}" , flags [ "X" ] ) ;
XRegisterBinaryBox . Text = ToBinStr ( flags [ "X" ] ) ;
2014-04-19 22:23:13 +00:00
2014-04-20 01:19:33 +00:00
YRegisterBox . Text = flags [ "Y" ] . ToString ( ) ;
YRegisterHexBox . Text = string . Format ( "{0:X2}" , flags [ "Y" ] ) ;
YRegisterBinaryBox . Text = ToBinStr ( flags [ "Y" ] ) ;
2014-04-19 22:23:13 +00:00
2014-04-20 01:19:33 +00:00
NFlagCheckbox . Checked = flags [ "Flag N" ] = = 1 ;
VFlagCheckbox . Checked = flags [ "Flag V" ] = = 1 ;
TFlagCheckbox . Checked = flags [ "Flag T" ] = = 1 ;
BFlagCheckbox . Checked = flags [ "Flag B" ] = = 1 ;
2014-04-19 22:23:13 +00:00
2014-04-20 01:19:33 +00:00
DFlagCheckbox . Checked = flags [ "Flag D" ] = = 1 ;
IFlagCheckbox . Checked = flags [ "Flag I" ] = = 1 ;
ZFlagCheckbox . Checked = flags [ "Flag Z" ] = = 1 ;
CFlagCheckbox . Checked = flags [ "Flag C" ] = = 1 ;
2014-04-19 19:01:13 +00:00
2014-05-27 01:33:22 +00:00
FrameLabel . Text = _core . Frame . ToString ( ) ;
ScanlineLabel . Text = _core . CurrentScanLine . ToString ( ) ;
TotalCyclesLabel . Text = _core . Cpu . TotalExecutedCycles . ToString ( ) ;
DistinctAccesLabel . Text = _core . DistinctAccessCount . ToString ( ) ;
LastAddressLabel . Text = _core . LastAddress . ToString ( ) ;
2014-04-20 01:19:33 +00:00
VSyncChexkbox . Checked = _core . IsVsync ;
VBlankCheckbox . Checked = _core . IsVBlank ;
UpdateTraceLog ( ) ;
2014-05-31 21:57:28 +00:00
_programmaticUpdateOfRegisterBoxes = false ;
2014-04-20 01:19:33 +00:00
}
2014-07-25 01:55:21 +00:00
public void FastUpdate ( )
{
/* TODO */
}
2014-04-20 01:19:33 +00:00
private void UpdateTraceLog ( )
{
2014-12-05 00:05:40 +00:00
var instructions = ( _core as IDebuggable ) . Tracer . TakeContents ( ) . Split ( '\n' ) ;
2014-04-20 01:19:33 +00:00
if ( ! string . IsNullOrWhiteSpace ( instructions [ 0 ] ) )
{
_instructions . AddRange ( instructions . Where ( str = > ! string . IsNullOrEmpty ( str ) ) ) ;
}
if ( _instructions . Count > = Global . Config . TraceLoggerMaxLines )
{
_instructions . RemoveRange ( 0 , _instructions . Count - Global . Config . TraceLoggerMaxLines ) ;
}
TraceView . ItemCount = _instructions . Count ;
2014-04-19 19:01:13 +00:00
}
private string ToBinStr ( int val )
{
return Convert . ToString ( ( uint ) val , 2 ) . PadLeft ( 8 , '0' ) ;
}
2014-04-20 01:19:33 +00:00
private void TraceView_QueryItemText ( int index , int column , out string text )
{
text = index < _instructions . Count ? _instructions [ index ] : string . Empty ;
}
2014-05-26 18:23:58 +00:00
private void BreakPointView_QueryItemText ( int index , int column , out string text )
{
text = string . Empty ;
switch ( column )
{
case 0 :
text = string . Format ( "{0:X4}" , Breakpoints [ index ] . Address ) ;
break ;
case 1 :
text = Breakpoints [ index ] . Type . ToString ( ) ;
break ;
}
}
private void BreakPointView_QueryItemBkColor ( int index , int column , ref Color color )
{
if ( index > = BreakpointView . ItemCount )
{
return ;
}
if ( column = = 0 )
{
if ( Breakpoints [ index ] . Active )
{
color = Color . LightCyan ;
}
else
{
color = BackColor ;
}
}
}
2014-05-26 19:38:02 +00:00
private void BreakpointCallback ( )
2014-04-19 19:01:13 +00:00
{
2014-05-26 19:38:02 +00:00
GlobalWin . MainForm . PauseEmulator ( ) ;
2014-04-19 19:01:13 +00:00
UpdateValues ( ) ;
}
2014-05-31 21:57:28 +00:00
private void SPRegisterBox_ValueChanged ( object sender , EventArgs e )
{
if ( ! _programmaticUpdateOfRegisterBoxes )
{
_core . SetCpuRegister ( "S" , ( int ) SPRegisterBox . Value ) ;
}
}
private void ARegisterBox_ValueChanged ( object sender , EventArgs e )
{
if ( ! _programmaticUpdateOfRegisterBoxes )
{
_core . SetCpuRegister ( "A" , ( int ) SPRegisterBox . Value ) ;
}
}
private void XRegisterBox_ValueChanged ( object sender , EventArgs e )
{
if ( ! _programmaticUpdateOfRegisterBoxes )
{
_core . SetCpuRegister ( "X" , ( int ) SPRegisterBox . Value ) ;
}
}
private void YRegisterBox_ValueChanged ( object sender , EventArgs e )
{
if ( ! _programmaticUpdateOfRegisterBoxes )
{
_core . SetCpuRegister ( "Y" , ( int ) SPRegisterBox . Value ) ;
}
}
2014-05-26 19:38:02 +00:00
#region Menu
2014-04-19 19:01:13 +00:00
2014-05-26 19:38:02 +00:00
private void ExitMenuItem_Click ( object sender , EventArgs e )
2014-04-20 16:19:08 +00:00
{
2014-05-26 19:38:02 +00:00
Close ( ) ;
2014-04-20 16:19:08 +00:00
}
private void OptionsSubMenu_DropDownOpened ( object sender , EventArgs e )
{
AutoloadMenuItem . Checked = Global . Config . Atari2600DebuggerAutoload ;
SaveWindowPositionMenuItem . Checked = Global . Config . Atari2600DebuggerSettings . SaveWindowPosition ;
TopmostMenuItem . Checked = Global . Config . Atari2600DebuggerSettings . TopMost ;
FloatingWindowMenuItem . Checked = Global . Config . Atari2600DebuggerSettings . FloatingWindow ;
}
private void AutoloadMenuItem_Click ( object sender , EventArgs e )
{
Global . Config . Atari2600DebuggerAutoload ^ = true ;
}
private void SaveWindowPositionMenuItem_Click ( object sender , EventArgs e )
{
Global . Config . Atari2600DebuggerSettings . SaveWindowPosition ^ = true ;
}
private void TopmostMenuItem_Click ( object sender , EventArgs e )
{
TopMost = Global . Config . Atari2600DebuggerSettings . TopMost ^ = true ;
}
private void FloatingWindowMenuItem_Click ( object sender , EventArgs e )
{
Global . Config . Atari2600DebuggerSettings . FloatingWindow ^ = true ;
RefreshFloatingWindowControl ( ) ;
}
private void RestoreDefaultsMenuItem_Click ( object sender , EventArgs e )
{
Size = new Size ( _defaultWidth , _defaultHeight ) ;
Global . Config . Atari2600DebuggerSettings = new ToolDialogSettings ( ) ;
TopMost = Global . Config . Atari2600DebuggerSettings . TopMost ;
RefreshFloatingWindowControl ( ) ;
}
2014-05-26 19:38:02 +00:00
#endregion
#region Dialog Events
2014-04-20 16:19:08 +00:00
protected override void OnShown ( EventArgs e )
{
RefreshFloatingWindowControl ( ) ;
base . OnShown ( e ) ;
}
2014-05-26 18:23:58 +00:00
2014-05-26 19:38:02 +00:00
private void StepBtn_Click ( object sender , EventArgs e )
{
2014-05-27 01:33:22 +00:00
var size = opsize [ _core . Cpu . PeekMemory ( _core . Cpu . PC ) ] ;
2014-05-26 19:38:02 +00:00
for ( int i = 0 ; i < size ; i + + )
{
_core . CycleAdvance ( ) ;
}
UpdateValues ( ) ;
}
private void ScanlineAdvanceBtn_Click ( object sender , EventArgs e )
{
_core . ScanlineAdvance ( ) ;
UpdateValues ( ) ;
}
private void FrameAdvButton_Click ( object sender , EventArgs e )
{
_core . FrameAdvance ( true , true ) ;
UpdateValues ( ) ;
}
private void AddBreakpointButton_Click ( object sender , EventArgs e )
{
var b = new AddBreakpointDialog ( ) ;
if ( b . ShowDialog ( ) = = DialogResult . OK )
{
2014-12-05 01:56:45 +00:00
Breakpoints . Add ( _core , b . Address , b . BreakType ) ;
2014-05-26 19:38:02 +00:00
}
BreakpointView . ItemCount = Breakpoints . Count ;
2014-05-30 01:10:10 +00:00
UpdateBreakpointRemoveButton ( ) ;
}
private void RemoveBreakpointButton_Click ( object sender , EventArgs e )
{
if ( BreakpointView . SelectedIndices . Count > 0 )
{
var items = SelectedItems . ToList ( ) ;
if ( items . Any ( ) )
{
foreach ( var item in items )
{
Breakpoints . Remove ( item ) ;
}
BreakpointView . ItemCount = Breakpoints . Count ;
UpdateBreakpointRemoveButton ( ) ;
}
}
}
private void BreakpointView_SelectedIndexChanged ( object sender , EventArgs e )
{
UpdateBreakpointRemoveButton ( ) ;
}
private void BreakpointView_KeyDown ( object sender , KeyEventArgs e )
{
if ( e . KeyCode = = Keys . Delete & & ! e . Control & & ! e . Alt & & ! e . Shift )
{
RemoveBreakpointButton_Click ( sender , e ) ;
}
2014-05-26 19:38:02 +00:00
}
private void RefreshFloatingWindowControl ( )
{
Owner = Global . Config . RamSearchSettings . FloatingWindow ? null : GlobalWin . MainForm ;
}
#endregion
2014-05-26 18:23:58 +00:00
// TODO: these can be generic to any debugger
#region Breakpoint Classes
public class AtariBreakpointList : List < AtariBreakpoint >
{
public Action Callback { get ; set ; }
2014-12-07 18:53:56 +00:00
public void Add ( Atari2600 core , uint address , MemoryCallbackType type )
2014-05-26 18:23:58 +00:00
{
2014-12-05 01:56:45 +00:00
Add ( new AtariBreakpoint ( core , Callback , address , type ) ) ;
2014-05-26 18:23:58 +00:00
}
}
public class AtariBreakpoint
{
private bool _active ;
2014-12-05 01:56:45 +00:00
private readonly Atari2600 _core ;
2014-05-26 18:23:58 +00:00
2014-12-07 18:53:56 +00:00
public AtariBreakpoint ( Atari2600 core , Action callBack , uint address , MemoryCallbackType type , bool enabled = true )
2014-05-26 18:23:58 +00:00
{
2014-12-05 01:56:45 +00:00
_core = core ;
2014-05-26 18:23:58 +00:00
Callback = callBack ;
Address = address ;
Active = enabled ;
if ( enabled )
{
AddCallback ( ) ;
}
}
public Action Callback { get ; set ; }
public uint Address { get ; set ; }
2014-12-07 18:53:56 +00:00
public MemoryCallbackType Type { get ; set ; }
2014-05-26 18:23:58 +00:00
public bool Active
{
get
{
return _active ;
}
set
{
if ( ! value )
{
RemoveCallback ( ) ;
}
if ( ! _active & & value ) // If inactive and changing to active
{
AddCallback ( ) ;
}
_active = value ;
}
}
private void AddCallback ( )
{
2014-12-07 19:09:36 +00:00
_core . MemoryCallbacks . Add ( new MemoryCallback ( Type , "" , Callback , Address ) ) ;
2014-05-26 18:23:58 +00:00
}
private void RemoveCallback ( )
{
2014-12-05 01:56:45 +00:00
_core . MemoryCallbacks . Remove ( Callback ) ;
2014-05-26 18:23:58 +00:00
}
}
#endregion
2014-04-19 19:01:13 +00:00
}
}