2017-07-20 11:51:05 +08:00
using Microsoft.Xna.Framework ;
using Microsoft.Xna.Framework.Graphics ;
using UIInfoSuite.Extensions ;
using StardewModdingAPI ;
using StardewModdingAPI.Events ;
using StardewValley ;
using StardewValley.Menus ;
using StardewValley.Quests ;
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Reflection ;
using System.Text ;
namespace UIInfoSuite.UIElements
{
class LocationOfTownsfolk : IDisposable
{
#region Members
private List < NPC > _townsfolk = new List < NPC > ( ) ;
private List < OptionsCheckbox > _checkboxes = new List < OptionsCheckbox > ( ) ;
private const int SocialPanelWidth = 190 ;
private const int SocialPanelXOffset = 160 ;
private SocialPage _socialPage ;
2018-05-06 12:21:18 +08:00
private String [ ] _friendNames ;
2017-07-20 11:51:05 +08:00
private readonly IDictionary < String , String > _options ;
private readonly IModHelper _helper ;
private static readonly Dictionary < String , KeyValuePair < int , int > > _mapLocations = new Dictionary < string , KeyValuePair < int , int > > ( )
{
{ "HarveyRoom" , new KeyValuePair < int , int > ( 677 , 304 ) } ,
{ "BathHouse_Pool" , new KeyValuePair < int , int > ( 576 , 60 ) } ,
{ "WizardHouseBasement" , new KeyValuePair < int , int > ( 196 , 352 ) } ,
{ "BugLand" , new KeyValuePair < int , int > ( 0 , 0 ) } ,
{ "Desert" , new KeyValuePair < int , int > ( 60 , 92 ) } ,
{ "Cellar" , new KeyValuePair < int , int > ( 0 , 0 ) } ,
{ "JojaMart" , new KeyValuePair < int , int > ( 872 , 280 ) } ,
{ "Tent" , new KeyValuePair < int , int > ( 784 , 128 ) } ,
{ "HaleyHouse" , new KeyValuePair < int , int > ( 652 , 408 ) } ,
{ "Hospital" , new KeyValuePair < int , int > ( 677 , 304 ) } ,
{ "FarmHouse" , new KeyValuePair < int , int > ( 470 , 260 ) } ,
2017-11-28 13:27:52 +08:00
{ "Farm" , new KeyValuePair < int , int > ( 470 , 260 ) } ,
2017-07-20 11:51:05 +08:00
{ "ScienceHouse" , new KeyValuePair < int , int > ( 732 , 148 ) } ,
{ "ManorHouse" , new KeyValuePair < int , int > ( 768 , 395 ) } ,
{ "AdventureGuild" , new KeyValuePair < int , int > ( 0 , 0 ) } ,
{ "SeedShop" , new KeyValuePair < int , int > ( 696 , 296 ) } ,
{ "Blacksmith" , new KeyValuePair < int , int > ( 852 , 388 ) } ,
{ "JoshHouse" , new KeyValuePair < int , int > ( 740 , 320 ) } ,
{ "SandyHouse" , new KeyValuePair < int , int > ( 40 , 40 ) } ,
{ "Tunnel" , new KeyValuePair < int , int > ( 0 , 0 ) } ,
{ "CommunityCenter" , new KeyValuePair < int , int > ( 692 , 204 ) } ,
{ "Backwoods" , new KeyValuePair < int , int > ( 460 , 156 ) } ,
{ "ElliottHouse" , new KeyValuePair < int , int > ( 826 , 550 ) } ,
{ "SebastianRoom" , new KeyValuePair < int , int > ( 732 , 148 ) } ,
{ "BathHouse_Entry" , new KeyValuePair < int , int > ( 576 , 60 ) } ,
{ "Greenhouse" , new KeyValuePair < int , int > ( 0 , 0 ) } ,
{ "Sewer" , new KeyValuePair < int , int > ( 380 , 596 ) } ,
{ "WizardHouse" , new KeyValuePair < int , int > ( 196 , 352 ) } ,
{ "Trailer" , new KeyValuePair < int , int > ( 780 , 360 ) } ,
2019-01-10 04:26:53 +08:00
{ "pamHouseUpgrade" , new KeyValuePair < int , int > ( 780 , 360 ) } ,
2017-07-20 11:51:05 +08:00
{ "Forest" , new KeyValuePair < int , int > ( 80 , 272 ) } ,
{ "Woods" , new KeyValuePair < int , int > ( 100 , 272 ) } ,
{ "WitchSwamp" , new KeyValuePair < int , int > ( 0 , 0 ) } ,
{ "ArchaeologyHouse" , new KeyValuePair < int , int > ( 892 , 416 ) } ,
{ "FishShop" , new KeyValuePair < int , int > ( 844 , 608 ) } ,
{ "Saloon" , new KeyValuePair < int , int > ( 714 , 354 ) } ,
{ "LeahHouse" , new KeyValuePair < int , int > ( 452 , 436 ) } ,
{ "Town" , new KeyValuePair < int , int > ( 680 , 360 ) } ,
{ "Mountain" , new KeyValuePair < int , int > ( 762 , 154 ) } ,
{ "BusStop" , new KeyValuePair < int , int > ( 516 , 224 ) } ,
{ "Railroad" , new KeyValuePair < int , int > ( 644 , 64 ) } ,
{ "SkullCave" , new KeyValuePair < int , int > ( 0 , 0 ) } ,
{ "BathHouse_WomensLocker" , new KeyValuePair < int , int > ( 576 , 60 ) } ,
{ "Beach" , new KeyValuePair < int , int > ( 790 , 550 ) } ,
{ "BathHouse_MensLocker" , new KeyValuePair < int , int > ( 576 , 60 ) } ,
{ "Mine" , new KeyValuePair < int , int > ( 880 , 100 ) } ,
{ "WitchHut" , new KeyValuePair < int , int > ( 0 , 0 ) } ,
{ "AnimalShop" , new KeyValuePair < int , int > ( 420 , 392 ) } ,
{ "SamHouse" , new KeyValuePair < int , int > ( 612 , 396 ) } ,
{ "WitchWarpCave" , new KeyValuePair < int , int > ( 0 , 0 ) } ,
{ "Club" , new KeyValuePair < int , int > ( 60 , 92 ) }
} ;
#endregion
public LocationOfTownsfolk ( IModHelper helper , IDictionary < String , String > options )
{
_helper = helper ;
_options = options ;
}
public void Dispose ( )
{
ToggleShowNPCLocationsOnMap ( false ) ;
}
public void ToggleShowNPCLocationsOnMap ( bool showLocations )
{
2018-12-25 12:36:39 +08:00
ExtendMenuIfNeeded ( ) ;
_helper . Events . Display . RenderedActiveMenu - = OnRenderedActiveMenu_DrawSocialPageOptions ;
_helper . Events . Display . RenderedActiveMenu - = OnRenderedActiveMenu_DrawNPCLocationsOnMap ;
_helper . Events . Input . ButtonPressed - = OnButtonPressed_ForSocialPage ;
_helper . Events . Display . MenuChanged - = OnMenuChanged ;
2017-07-20 11:51:05 +08:00
if ( showLocations )
{
2018-12-25 12:36:39 +08:00
_helper . Events . Display . RenderedActiveMenu + = OnRenderedActiveMenu_DrawSocialPageOptions ;
_helper . Events . Display . RenderedActiveMenu + = OnRenderedActiveMenu_DrawNPCLocationsOnMap ;
_helper . Events . Input . ButtonPressed + = OnButtonPressed_ForSocialPage ;
_helper . Events . Display . MenuChanged + = OnMenuChanged ;
2017-07-20 11:51:05 +08:00
}
}
2018-12-25 12:36:39 +08:00
/// <summary>Raised after a game menu is opened, closed, or replaced.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnMenuChanged ( object sender , MenuChangedEventArgs e )
2017-07-20 11:51:05 +08:00
{
2018-12-25 12:36:39 +08:00
ExtendMenuIfNeeded ( ) ;
2017-07-20 11:51:05 +08:00
}
2018-12-25 12:36:39 +08:00
private void ExtendMenuIfNeeded ( )
2017-07-20 11:51:05 +08:00
{
if ( Game1 . activeClickableMenu is GameMenu )
{
List < IClickableMenu > clickableMenuList = typeof ( GameMenu )
. GetField ( "pages" , BindingFlags . Instance | BindingFlags . NonPublic )
. GetValue ( Game1 . activeClickableMenu ) as List < IClickableMenu > ;
foreach ( var menu in clickableMenuList )
{
if ( menu is SocialPage )
{
_socialPage = menu as SocialPage ;
2018-05-07 04:41:04 +08:00
_friendNames = ( typeof ( SocialPage ) . GetField ( "names" , BindingFlags . Instance | BindingFlags . NonPublic ) . GetValue ( menu ) as List < object > )
. Select ( name = > name . ToString ( ) )
. ToArray ( ) ;
2017-07-20 11:51:05 +08:00
break ;
}
}
_townsfolk . Clear ( ) ;
foreach ( var location in Game1 . locations )
{
foreach ( var npc in location . characters )
{
2018-05-06 12:21:18 +08:00
if ( Game1 . player . friendshipData . ContainsKey ( npc . Name ) )
2017-07-20 11:51:05 +08:00
_townsfolk . Add ( npc ) ;
}
}
_checkboxes . Clear ( ) ;
foreach ( var friendName in _friendNames )
{
2018-05-06 12:21:18 +08:00
int hashCode = friendName . GetHashCode ( ) ;
2017-07-20 11:51:05 +08:00
OptionsCheckbox checkbox = new OptionsCheckbox ( "" , hashCode ) ;
_checkboxes . Add ( checkbox ) ;
2017-11-28 13:27:52 +08:00
//default to on
bool optionForThisFriend = true ;
2018-05-06 12:21:18 +08:00
if ( ! Game1 . player . friendshipData . ContainsKey ( friendName ) )
2017-07-20 11:51:05 +08:00
{
checkbox . greyedOut = true ;
2017-11-28 13:27:52 +08:00
optionForThisFriend = false ;
}
else
{
String optionValue = _options . SafeGet ( hashCode . ToString ( ) ) ;
if ( String . IsNullOrEmpty ( optionValue ) )
{
_options [ hashCode . ToString ( ) ] = optionForThisFriend . ToString ( ) ;
}
else
{
optionForThisFriend = optionValue . SafeParseBool ( ) ;
}
2017-07-20 11:51:05 +08:00
}
2017-11-28 13:27:52 +08:00
checkbox . isChecked = optionForThisFriend ;
2017-07-20 11:51:05 +08:00
}
}
}
2018-12-25 12:36:39 +08:00
/// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnButtonPressed_ForSocialPage ( object sender , ButtonPressedEventArgs e )
2017-07-20 11:51:05 +08:00
{
2018-12-25 12:36:39 +08:00
if ( Game1 . activeClickableMenu is GameMenu & & ( e . Button = = SButton . MouseLeft | | e . Button = = SButton . ControllerA ) )
2017-07-20 11:51:05 +08:00
{
CheckSelectedBox ( ) ;
}
}
private void CheckSelectedBox ( )
{
if ( Game1 . activeClickableMenu is GameMenu )
{
int slotPosition = ( int ) typeof ( SocialPage )
. GetField ( "slotPosition" , BindingFlags . Instance | BindingFlags . NonPublic )
. GetValue ( _socialPage ) ;
for ( int i = slotPosition ; i < slotPosition + 5 ; + + i )
{
OptionsCheckbox checkbox = _checkboxes [ i ] ;
if ( checkbox . bounds . Contains ( Game1 . getMouseX ( ) , Game1 . getMouseY ( ) ) & &
! checkbox . greyedOut )
{
checkbox . isChecked = ! checkbox . isChecked ;
_options [ checkbox . whichOption . ToString ( ) ] = checkbox . isChecked . ToString ( ) ;
Game1 . playSound ( "drumkit6" ) ;
}
}
}
}
2018-12-25 12:36:39 +08:00
/// <summary>When a menu is open (<see cref="Game1.activeClickableMenu"/> isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnRenderedActiveMenu_DrawNPCLocationsOnMap ( object sender , RenderedActiveMenuEventArgs e )
2017-07-20 11:51:05 +08:00
{
2018-12-25 12:36:39 +08:00
if ( Game1 . activeClickableMenu is GameMenu gameMenu )
2017-07-20 11:51:05 +08:00
{
if ( gameMenu . currentTab = = 3 )
{
2017-11-28 13:27:52 +08:00
List < String > namesToShow = new List < string > ( ) ;
2017-07-20 11:51:05 +08:00
foreach ( var character in _townsfolk )
{
try
{
2018-05-06 12:21:18 +08:00
int hashCode = character . Name . GetHashCode ( ) ;
2017-11-28 13:27:52 +08:00
2017-07-20 11:51:05 +08:00
bool drawCharacter = _options . SafeGet ( hashCode . ToString ( ) ) . SafeParseBool ( ) ;
if ( drawCharacter )
{
2018-12-25 12:36:39 +08:00
KeyValuePair < int , int > location = new KeyValuePair < int , int > ( ( int ) character . Position . X , ( int ) character . position . Y ) ;
2018-05-06 12:21:18 +08:00
String locationName = character . currentLocation ? . Name ? ? character . DefaultMap ;
2017-07-20 11:51:05 +08:00
2017-11-28 13:27:52 +08:00
switch ( locationName )
2017-07-20 11:51:05 +08:00
{
2017-11-28 13:27:52 +08:00
case "Town" :
case "Forest" :
{
int xStart = 0 ;
int yStart = 0 ;
int areaWidth = 0 ;
int areaHeight = 0 ;
switch ( locationName )
{
case "Town" :
{
xStart = 595 ;
yStart = 163 ;
areaWidth = 345 ;
areaHeight = 330 ;
break ;
}
case "Forest" :
{
xStart = 183 ;
yStart = 378 ;
areaWidth = 319 ;
areaHeight = 261 ;
break ;
}
}
xTile . Map map = character . currentLocation . Map ;
float xScale = ( float ) areaWidth / ( float ) map . DisplayWidth ;
float yScale = ( float ) areaHeight / ( float ) map . DisplayHeight ;
float scaledX = character . position . X * xScale ;
float scaledY = character . position . Y * yScale ;
int xPos = ( int ) scaledX + xStart ;
int yPos = ( int ) scaledY + yStart ;
location = new KeyValuePair < int , int > ( xPos , yPos ) ;
break ;
}
default :
{
_mapLocations . TryGetValue ( locationName , out location ) ;
break ;
}
2017-07-20 11:51:05 +08:00
}
2017-11-28 13:27:52 +08:00
//if (character.currentLocation.Name == "Town")
//{
// String locationName = character.currentLocation.Name;
// xTile.Map map = character.currentLocation.Map;
// int xStart = 595;
// int yStart = 163;
// int townWidth = 345;
// int townHeight = 330;
// float xScale = (float)townWidth / (float)map.DisplayWidth;
// float yScale = (float)townHeight / (float)map.DisplayHeight;
// float scaledX = character.position.X * xScale;
// float scaledY = character.position.Y * yScale;
// int xPos = (int)scaledX + xStart;
// int yPos = (int)scaledY + yStart;
// location = new KeyValuePair<int, int>(xPos, yPos);
//}
//else
//{
// _mapLocations.TryGetValue(character.currentLocation.name, out location);
//}
2017-07-20 11:51:05 +08:00
Rectangle headShot = character . GetHeadShot ( ) ;
int xBase = Game1 . activeClickableMenu . xPositionOnScreen - 158 ;
int yBase = Game1 . activeClickableMenu . yPositionOnScreen - 40 ;
int x = xBase + location . Key ;
int y = yBase + location . Value ;
Color color = character . CurrentDialogue . Count < = 0 ?
Color . Gray : Color . White ;
ClickableTextureComponent textureComponent =
new ClickableTextureComponent (
2018-05-06 12:21:18 +08:00
character . Name ,
2017-07-20 11:51:05 +08:00
new Rectangle ( x , y , 0 , 0 ) ,
null ,
2018-05-06 12:21:18 +08:00
character . Name ,
character . Sprite . Texture ,
2017-07-20 11:51:05 +08:00
headShot ,
2.3f ) ;
2017-11-28 13:27:52 +08:00
float headShotScale = 2f ;
2017-07-20 11:51:05 +08:00
Game1 . spriteBatch . Draw (
2018-05-06 12:21:18 +08:00
character . Sprite . Texture ,
2017-07-20 11:51:05 +08:00
new Vector2 ( x , y ) ,
new Rectangle ? ( headShot ) ,
color ,
0.0f ,
Vector2 . Zero ,
2017-11-28 13:27:52 +08:00
headShotScale ,
2017-07-20 11:51:05 +08:00
SpriteEffects . None ,
1f ) ;
2017-11-28 13:27:52 +08:00
int mouseX = Game1 . getMouseX ( ) ;
int mouseY = Game1 . getMouseY ( ) ;
if ( mouseX > = x & & mouseX < = x + headShot . Width * headShotScale & &
mouseY > = y & & mouseY < = y + headShot . Height * headShotScale )
{
namesToShow . Add ( character . displayName ) ;
}
2017-07-20 11:51:05 +08:00
foreach ( var quest in Game1 . player . questLog )
{
2018-05-17 12:13:19 +08:00
if ( quest . accepted . Value & & quest . dailyQuest . Value & & ! quest . completed . Value )
2017-07-20 11:51:05 +08:00
{
bool isQuestTarget = false ;
2018-05-17 12:13:19 +08:00
switch ( quest . questType . Value )
2017-07-20 11:51:05 +08:00
{
2018-05-17 12:13:19 +08:00
case 3 : isQuestTarget = ( quest as ItemDeliveryQuest ) . target . Value = = character . Name ; break ;
case 4 : isQuestTarget = ( quest as SlayMonsterQuest ) . target . Value = = character . Name ; break ;
case 7 : isQuestTarget = ( quest as FishingQuest ) . target . Value = = character . Name ; break ;
case 10 : isQuestTarget = ( quest as ResourceCollectionQuest ) . target . Value = = character . Name ; break ;
2017-07-20 11:51:05 +08:00
}
if ( isQuestTarget )
Game1 . spriteBatch . Draw (
Game1 . mouseCursors ,
new Vector2 ( x + 10 , y - 12 ) ,
new Rectangle ( 394 , 495 , 4 , 10 ) ,
Color . White ,
0.0f ,
Vector2 . Zero ,
3f ,
SpriteEffects . None ,
1f ) ;
}
}
}
}
catch ( Exception ex )
{
ModEntry . MonitorObject . Log ( ex . Message + Environment . NewLine + ex . StackTrace , LogLevel . Error ) ;
}
}
2017-11-28 13:27:52 +08:00
if ( namesToShow . Count > 0 )
{
StringBuilder text = new StringBuilder ( ) ;
int longestLength = 0 ;
foreach ( String name in namesToShow )
{
text . AppendLine ( name ) ;
longestLength = Math . Max ( longestLength , ( int ) Math . Ceiling ( Game1 . smallFont . MeasureString ( name ) . Length ( ) ) ) ;
}
int windowHeight = Game1 . smallFont . LineSpacing * namesToShow . Count + 25 ;
Vector2 windowPos = new Vector2 ( Game1 . getMouseX ( ) + 40 , Game1 . getMouseY ( ) - windowHeight ) ;
IClickableMenu . drawTextureBox (
Game1 . spriteBatch ,
( int ) windowPos . X ,
( int ) windowPos . Y ,
longestLength + 30 ,
Game1 . smallFont . LineSpacing * namesToShow . Count + 25 ,
Color . White ) ;
Game1 . spriteBatch . DrawString (
Game1 . smallFont ,
text ,
new Vector2 ( windowPos . X + 17 , windowPos . Y + 17 ) ,
Game1 . textShadowColor ) ;
Game1 . spriteBatch . DrawString (
Game1 . smallFont ,
text ,
new Vector2 ( windowPos . X + 15 , windowPos . Y + 15 ) ,
Game1 . textColor ) ;
}
//The cursor needs to show up in front of the character faces
2018-05-21 07:38:50 +08:00
Tools . DrawMouseCursor ( ) ;
2017-07-20 11:51:05 +08:00
String hoverText = ( String ) typeof ( MapPage )
. GetField (
"hoverText" ,
BindingFlags . Instance | BindingFlags . NonPublic )
. GetValue ( ( ( List < IClickableMenu > ) typeof ( GameMenu )
. GetField ( "pages" , BindingFlags . Instance | BindingFlags . NonPublic )
. GetValue ( gameMenu ) ) [ gameMenu . currentTab ] ) ;
IClickableMenu . drawHoverText (
Game1 . spriteBatch ,
hoverText ,
Game1 . smallFont ) ;
}
}
}
2018-12-25 12:36:39 +08:00
/// <summary>When a menu is open (<see cref="Game1.activeClickableMenu"/> isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnRenderedActiveMenu_DrawSocialPageOptions ( object sender , RenderedActiveMenuEventArgs e )
2017-07-20 11:51:05 +08:00
{
2018-12-25 12:36:39 +08:00
if ( Game1 . activeClickableMenu is GameMenu gameMenu & & gameMenu . currentTab = = 2 )
2017-07-20 11:51:05 +08:00
{
Game1 . drawDialogueBox (
Game1 . activeClickableMenu . xPositionOnScreen - SocialPanelXOffset ,
Game1 . activeClickableMenu . yPositionOnScreen ,
SocialPanelWidth ,
Game1 . activeClickableMenu . height ,
false ,
true ) ;
int slotPosition = ( int ) typeof ( SocialPage )
. GetField ( "slotPosition" , BindingFlags . Instance | BindingFlags . NonPublic )
. GetValue ( _socialPage ) ;
int yOffset = 0 ;
2018-05-17 12:13:19 +08:00
for ( int i = slotPosition ; i < slotPosition + 5 & & i < _friendNames . Length ; + + i )
2017-07-20 11:51:05 +08:00
{
OptionsCheckbox checkbox = _checkboxes [ i ] ;
checkbox . bounds . X = Game1 . activeClickableMenu . xPositionOnScreen - 60 ;
checkbox . bounds . Y = Game1 . activeClickableMenu . yPositionOnScreen + 130 + yOffset ;
checkbox . draw ( Game1 . spriteBatch , 0 , 0 ) ;
yOffset + = 112 ;
Color color = checkbox . isChecked ? Color . White : Color . Gray ;
Game1 . spriteBatch . Draw (
Game1 . mouseCursors ,
new Vector2 ( checkbox . bounds . X - 50 , checkbox . bounds . Y ) ,
new Rectangle ( 80 , 0 , 16 , 16 ) ,
color ,
0.0f ,
Vector2 . Zero ,
3f ,
SpriteEffects . None ,
1f ) ;
if ( yOffset ! = 560 )
{
Game1 . spriteBatch . Draw (
Game1 . staminaRect ,
new Rectangle (
checkbox . bounds . X - 50 ,
checkbox . bounds . Y + 72 ,
SocialPanelWidth / 2 - 6 ,
4 ) ,
Color . SaddleBrown ) ;
Game1 . spriteBatch . Draw (
Game1 . staminaRect ,
new Rectangle (
checkbox . bounds . X - 50 ,
checkbox . bounds . Y + 76 ,
SocialPanelWidth / 2 - 6 ,
4 ) ,
Color . BurlyWood ) ;
}
2019-01-07 13:25:05 +08:00
if ( ! Game1 . options . hardwareCursor )
{
Game1 . spriteBatch . Draw (
Game1 . mouseCursors ,
new Vector2 (
Game1 . getMouseX ( ) ,
Game1 . getMouseY ( ) ) ,
Game1 . getSourceRectForStandardTileSheet (
Game1 . mouseCursors ,
Game1 . mouseCursor ,
16 ,
16 ) ,
Color . White ,
0.0f ,
Vector2 . Zero ,
Game1 . pixelZoom + ( Game1 . dialogueButtonScale / 150.0f ) ,
SpriteEffects . None ,
1f ) ;
}
2017-07-20 11:51:05 +08:00
if ( checkbox . bounds . Contains ( Game1 . getMouseX ( ) , Game1 . getMouseY ( ) ) )
IClickableMenu . drawHoverText (
Game1 . spriteBatch ,
"Track on map" ,
Game1 . dialogueFont ) ;
}
}
}
}
}