//============================================================================= // AICommand_Debug //============================================================================= // Empty (debug) AI command //============================================================================= // Killing Floor 2 // Copyright (C) 2015 Tripwire Interactive LLC //============================================================================= class AICommand_Debug extends AICommand within KFAIController; var bool bTestStepAside; var bool bNoFocus; var bool bOldDebugPostRenderInfo; var vector DebugMoveLocation; var bool bTempDisableHitWall; var bool bStoppedRotationTowardEnemy; var KFPawn DebuggingPlayer; var float MaxRoamDist; var float RoamStartTime; var float RoamEnvelopeOuter; var float RoamEnvelopeInner; var float RoamWaitMin; var float RoamWaitMax; var actor LastDebugGoal; //var array VisitedJumpSpots; /********************************************************************************************* * Initialization **********************************************************************************************/ static function bool Debug( KFAIController AI ) { local AICommand_Debug Cmd; if( AI != None ) { Cmd = new(AI) Default.Class; if( Cmd != None ) { AI.PushCommand(Cmd); return true; } } return false; } function Pushed() { `AILog( "Entering debug mode", 'Command_Debug' ); bHasDebugCommand = true; //Focus = none; ClearTimer( nameof(Debug_CheckRecentMoveTime), Outer ); //SetFocalPoint( Pawn.Location + vector( Pawn.Rotation ) * 512.f ); // if( Enemy != none && Enemy.IsHumanControlled() ) // Enemy = none; bOldDebugPostRenderInfo = bDebug_PostRenderInfo; // bDebug_PostRenderInfo = true; // SetPostRendering( true ); GotoState( 'Wait' ); } function Resumed( Name OldCommandName ) { DisableMeleeRangeEventProbing(); // if( OldCommandName == 'AICommand_StepAside' ) // { // bNoFocus = false; // } if( OldCommandName == 'AICommand_MoveToGoal' ) { `AILog( "Resuming "$self$" after "$OldCommandName$" ChildStatus: "$ChildStatus$" LastDebugGoal: "$LastDebugGoal ); } if( OldCommandName == 'AICommand_MoveToGoal' && (ChildStatus == 'Aborted' || ChildStatus == 'Failure') && Pawn != none && Pawn.IsAliveAndWell() ) { `AILog( "Resuming from failed MoveToGoal command", 'PathWarning' ); if( LastDebugGoal != none ) { Pawn.ZeroMovementVariables(); StopLatentExecution(); msg( "Debug NPC ["$Pawn$"] retrying path to "$LastDebugGoal$", "$VSize(LastDebugGoal.Location - Pawn.Location)$" units away." ); // SetMoveGoal( LastDebugGoal, none, false, 0.f, false, true, true ); } } super.Resumed( OldCommandName ); } function GrabTestTimer() { if( VSizeSq( Enemy.Location - Pawn.Location ) < 48400.f ) // 220UU { bAllowedToAttack = true; DoGrabAttack( Enemy ); bAllowedToAttack = false; } } function Popped() { bDebug_PostRenderInfo = bOldDebugPostRenderInfo; SetPostRendering( bDebug_PostRenderInfo ); super.Popped(); bHasDebugCommand = false; `AILog( "Exiting Debug Mode", 'Command_Debug' ); } function bool NotifyPlayerBecameVisible( Pawn VisiblePlayer ) { if( CachedChildCommand != None ) { `AILog( GetFuncName()$"() Seen: "$VisiblePlayer$" notifying "$CachedChildCommand$" and letting it handle the event.", 'Command_Debug' ); return CachedChildCommand.NotifyPlayerBecameVisible( VisiblePlayer ); } `AILog( GetFuncName()$" "$VisiblePlayer$" ignoring this event", 'Command_Debug' ); return true; } function Debug_Charge(); function Debug_MoveForward(); function Debug_Vision( Pawn P ); function Debug_Flank(); function Debug_Hide(); function Debug_Enemy( Pawn P ); function Debug_StepAside( optional bool bOn = true ); function Debug_Steering(); function Debug_Wander( optional float WanderDuraion=-1.f ); function Debug_DebugLines(); function Debug_Leap(); function Debug_DebugNodes(); function Debug_CrawlerAttack(); function Debug_LOS(); /********************************************************************************************* * Debugging State (abstract base state) ********************************************************************************************* */ state Debugging { event BeginState( name PreviousStateName ) { `AILog( GetFuncName()$"() in "$GetStateName()$" Previous: "$PreviousStateName, 'Command_Debug' ); } function InitState() { `AILog( GetFuncName()$"() in "$GetStateName(), 'Command_Debug' ); // Focus = none; // SetFocalPoint( vector(Pawn.Rotation) * 300 ); // DisableSeePlayer(); AIActionStatus = "Awaiting debug command"; } event EndState( name NextStateName ) { `AILog( GetFuncName()$"() in "$GetStateName()$" Next: "$NextStateName, 'Command_Debug' ); } function PushedState() { `AILog( GetFuncName()$"() in "$GetStateName(), 'Command_Debug' ); } function PausedState() { `AILog( GetFuncName()$"() in "$GetStateName(), 'Command_Debug' ); } function ContinuedState() { `AILog( GetFuncName()$"() in "$GetStateName(), 'Command_Debug' ); } function PoppedState() { `AILog( GetFuncName()$"() in "$GetStateName(), 'Command_Debug' ); } function Debug_CrawlerAttack() { local PlayerController P; foreach WorldInfo.AllActors( class'PlayerController', P ) { if( P.Pawn != None ) { Enemy = P.Pawn; } } if( Enemy != none ) { bAllowedToAttack = true; } EnableMeleeRangeEventProbing(); Pawn.SetPhysics( PHYS_Walking ); ReadyToMelee(); } } state MoveToRandomDebugNodes { function Debug_DebugNodes() { StopAllLatentMovement(); PopState(); } function GetGoal() { // local KFPathnode KFPN; // local array NavGoals; // foreach allactors( class'KFPathnode', KFPN ) // { // if( KFPN.DebugTag != '' && VSize(KFPN.Location - Pawn.Location) > 750.f ) // { //// NavGoals.AddItem(KFPN); // } // } // SetMoveGoal( NavGoals[ Rand(NavGoals.Length) ] ); } Begin: WaitForLanding(); GetGoal(); Sleep( RandRange(3,8) ); Goto( 'Begin' ); } /********************************************************************************************* * DebugLOS state **********************************************************************************************/ state DebugLOS { function BeginState( name PreviousStateName ) { StartLOSTimer(); } function PushedState() { StartLOSTimer(); } function StartLOSTimer() { Disable( 'SeePlayer' ); Disable( 'HearNoise' ); SetTimer( 0.5f, true, nameof(CheckLOS), self ); } function EndState( name NextStateName ) { ClearTimer( nameof(CheckLOS), self ); } function PoppedState() { ClearTimer( nameof(CheckLOS), self ); } function CheckLOS() { local KFPlayerController KFPC; foreach WorldInfo.AllControllers( class'KFPlayerController', KFPC ) { if( KFPC.Pawn != none && CanSee( KFPC.Pawn ) && Pawn.LineOfSightTo( KFPC.Pawn ) ) { msg( self$" CAN SEE "$KFPC.Pawn ); } else { msg( self$" CANNOT SEE "$KFPC.Pawn ); } } } } /********************************************************************************************* * Wait state (auto state) ********************************************************************************************* */ auto state Wait extends Debugging { function BeginState( name PreviousStateName ) { super.BeginState( PreviousStateName ); //Steering.EnableDefaultAcceleration(); InitState(); } function ContinuedState() { super.ContinuedState(); InitState(); } /** Called from cheat manager */ function Debug_MoveForward() { PushState( 'DebugMoveForward', 'Ready' ); } /** Called from cheat manager */ function Debug_Hide() { PushState( 'DebugHiding' ); } /** Called from cheat manager */ function Debug_Vision( Pawn P ) { DebuggingPlayer = KFPawn(P); PushState( 'DebugVision' ); } /** Called from cheat manager */ function Debug_Enemy( Pawn P ) { DebuggingPlayer = KFPawn(P); PushState( 'DebugEnemy' ); } function Debug_Leap() { PushState( 'DebugLeap' ); } function Debug_LOS() { PushState( 'DebugLOS' ); } function Debug_DebugNodes() { PushState( 'MoveToRandomDebugNodes' ); } /** Called from cheat manager */ function Debug_StepAside( optional bool bOn = true ) { if( !bOn ) { msg( Outer$" : StepAside debugging is already turned off." ); } else { PushState( 'DebugStepAside' ); } } /** Called from cheat manager */ function Debug_Steering() { PushState( 'DebugSteering' ); } /** Called from cheat manager */ function Debug_Wander( optional float WanderDuraion=-1.f ) { class'AICommand_Wander'.static.BeginWander( outer, WanderDuraion, GetALocalPlayerController().Pawn, false ); } /** Called from cheat manager */ function Debug_DebugLines() { PushState( 'DebugDebugLines' ); } } // DebugTurn state state DebugTurn { event HandleAICommandSpecialAction() { if( GetFocalPoint() != vect(0,0,0) ) { DrawDebugLine( Pawn.Location, Pawn.Location + normal( GetFocalPoint() - Pawn.Location ) * 1024.f, 0, 255, 0, false ); } } function PushedState() { SetFocalPoint(vect(0,0,0) ); Focus = none; } } /********************************************************************************************* * DebugLeap state **********************************************************************************************/ state DebugLeap { function PushedState() { local KFWallPathNode KFWP, BestKFWP; local float BestDist; foreach WorldInfo.AllNavigationPoints( class'KFWallPathNode', KFWP ) { if( BestKFWP == None || VSize(KFWP.Location - Pawn.Location) < BestDist ) { if( KFWP.Location.Z > 350.f ) { BestDist = VSize(KFWP.Location - Pawn.Location); BestKFWP = KFWP; } } } MoveTarget = BestKFWP; // OldFloor = vect(0,0,1); //GetAxes(Pawn.Rotation,ViewX,ViewY,ViewZ); //GroundPitch = 0; Pawn.bCrawler = true; } function bool NotifyHitWall( vector HitNormal, actor Wall ) { Pawn.SetPhysics( PHYS_Spider ); Pawn.SetBase( Wall, HitNormal ); return false; } // function DoWallLeap() // { // //LeapTarget = BestKFWP; // // OldFloor = vect(0,0,1); // GetAxes(Pawn.Rotation,ViewX,ViewY,ViewZ); // //GroundPitch = 0; // Pawn.bCrawler = true; // //KFAIController_ZedCrawler(Outer).DoWallLeap( BestKFWP ); // //Pawn.SetCollisionSize(Pawn.Default.CollisionHeight,Pawn.Default.CollisionHeight); // } Begin: if(Pawn.Physics == PHYS_Falling ) { WaitForLanding(); } Pawn.SetPhysics(PHYS_Walking); PushState( 'RotateToMoveTarget' ); Focus = none; SetFocalPoint(vect(0,0,0)); Sleep( 1.f ); //KFAIController_ZedCrawler(Outer).DoWallLeap( MoveTarget ); Sleep( 0.1f ); if( Pawn.Physics == PHYS_Falling ) { WaitForLanding(); } } /********************************************************************************************* * DebugSteering state - draws separation steering forces, etc. ********************************************************************************************* */ state DebugSteering extends Debugging { function PushedState() { super.PushedState(); AIActionStatus = "In DebugSteering state"; bDebug_DrawSeparationSteering = true; } function Debug_Steering() { PopState(); } function PoppedState() { bDebug_DrawSeparationSteering = false; super.PoppedState(); } } /********************************************************************************************* * DebugStepAside state - NPC will step aside when bumped or on AIDebugStepSide command ********************************************************************************************* */ state DebugStepAside extends Debugging { function PushedState() { super.PushedState(); AIActionStatus = "In DebugStepAside state"; msg( Outer$" in DebugStepAside state. Forcing NPC collision will invoke StepAside command, or type AIDEBUGSTEPASIDE again to force NPC to step aside with no collision." ); bIgnoreStepAside = false; } function Debug_StepAside( optional bool bOn = true ) { if( !bOn ) { msg( Outer$" : Turning off StepAside debugging" ); PopState(); } else if( CachedChildCommand == none ) { msg( Outer$" : random step aside" ); } } function bool NotifyBump( Actor Other, vector HitNormal ) { local bool bPrevIgnoreStepAside; local bool bResult; `AILog( GetFuncName()$" bumped into "$Other, 'Command_Debug' ); if( Pawn(Other) != none ) { `AILog( GetFuncName()$" bumped into "$Other$", starting StepAside command", 'Command_Debug' ); bPrevIgnoreStepAside = Outer.bIgnoreStepAside; Outer.bIgnoreStepAside = false; StepAsideFor( Pawn(Other),HitNormal ); Outer.bIgnoreStepAside = bPrevIgnoreStepAside; bResult = true; } DisableBump( 1.f ); return bResult; } } /********************************************************************************************* * DebugJumpPoints state - move to and attempt to use any KFJumpPoints in the level ********************************************************************************************* */ //state DebugJumpPoints extends Debugging //{ // function PushedState() // { // super.PushedState(); // AIActionStatus = "In DebugJumpPoints state"; // DisableSeePlayer(); // } // function KFPathnode PickJumpDest() // { // local KFJumpSpot KFJS; // local KFJumpSpot ClosestKFJS; // local float BestDist; // foreach WorldInfo.AllNavigationPoints( class'KFJumpSpot', KFJS ) // { // if( !KFJS.bDebug_Visited ) // { // if( BestDist == 0 || VSize(KFJS.Location - Pawn.Location) < BestDist ) // { // BestDist = VSize(KFJS.Location - Pawn.Location); // ClosestKFJS = KFJS; // } // } // } // return KFPathnode(ClosestKFJS.JumpTarget); // } //Begin: // if( Pawn.Physics == PHYS_Falling ) // { // WaitForLanding(); // } // Pawn.SetPhysics( PHYS_Walking ); // SetMoveGoal( PickJumpDest(), none, false, 0, false, true, true, false, false ); // if( ChildStatus == 'Failure' ) // { // PopState(); // } // PopState(); //} /********************************************************************************************* * DebugEnemy state - Testing NPC threat evaluations and enemy changes ********************************************************************************************* */ function EvaluateThreats(); state DebugEnemy extends Debugging { function PushedState() { super.PushedState(); EnableSeePlayer(); AIActionStatus = "In DebugEnemy state"; SetTimer( 1.f, true, nameof(EvaluateThreats), self ); } function EvaluateThreats() { local KFPlayerController KFPC; foreach WorldInfo.AllControllers( class'KFPlayerController', KFPC ) { EvaluateThreatFrom( KFPC.Pawn ); } } // function bool NotifyPlayerBecameVisible( Pawn VisiblePlayer ) // { // if( Enemy == none ) // { // ChangeEnemy(VisiblePlayer); // Focus = Enemy; // PushState('RotateToFocus'); // return true; // } // // if( Enemy == VisiblePlayer ) // { // return false; // } // // if( EvaluateThreatFrom(Enemy) < EvaluateThreatFrom(VisiblePlayer) ) // { // msg( "CHANGING TO "$VisiblePlayer ); // ChangeEnemy(VisiblePlayer); // Focus = Enemy; // PushState('RotateToFocus'); // } // // return false; // } simulated function DrawDebug( HUD HUD, name Category ) { if( DebuggingPlayer != none ) { DrawDebugText( HUD, "LOS: "$LineOfSightTo(DebuggingPlayer)$" CanSee: "$CanSee(DebuggingPlayer) ); DrawDebugText( HUD, "VISIBLE BY TRACE: "$IsPawnVisibleViaTrace(DebuggingPlayer) ); } } } /********************************************************************************************* * DebugHiding state - Hide from player who issued command! ********************************************************************************************* */ state DebugHiding extends Debugging { function PushedState() { super.PushedState(); MyKFPawn.SetSprinting(true); AIActionStatus = "In DebugHiding state"; if( Enemy == none ) { Enemy = GetClosestEnemy(); } } function Debug_Hide() { PopState(); } function bool CheckRetreat() { local Pawn ClosestEnemy; ClosestEnemy = GetClosestEnemy(); if( ClosestEnemy != none && CanSee( ClosestEnemy ) ) { class'Path_AlongLine'.static.AlongLine(Pawn, -Normal(ClosestEnemy.Location - Pawn.Location)); class'Goal_AwayFromPosition'.static.FleeFrom(Pawn, ClosestEnemy.Location, 4000); if( FindPathToward(ClosestEnemy) != None ) { AIActionStatus = "Hiding from "$ClosestEnemy$" at "$RouteGoal; // SetMoveGoal( RouteGoal,, false,128.f, true, true ); return true; } } return false; } Begin: if( Pawn.Physics == PHYS_Falling ) { WaitForLanding(); } if( CheckRetreat() ) { Sleep( 0.25f ); Goto( 'Begin' ); } MyKFPawn.SetSprinting(false); PopState(); } function CheckVision(); /********************************************************************************************* * DebugVision state (renders pawn view cone, etc.) ********************************************************************************************* */ state DebugVision extends Debugging { function PushedState() { super.PushedState(); msg( "Debugging "$MyKFPawn$" vision, current PeripheralVision: "$MyKFPawn.PeripheralVision$" Current SightRadius: "$MyKFPawn.SightRadius$" Checking LOS every 0.5 seconds" ); SetTimer( 2.f, true, nameof(CheckVision), self ); AIActionStatus = "In DebugVision state"; EnableSeePlayer(); } function bool NotifyEnemyBecameVisible( Pawn VisiblePlayer ) { msg( "YOU ARE VISIBLE : "$normal( Enemy.Location - Pawn.Location ) dot vector(Pawn.Rotation) ); Outer.Enable( 'EnemyNotVisible' ); Outer.Disable( 'SeePlayer' ); return true; } function bool NotifyEnemyNotVisible() { msg( "YOU ARE NO LONGER VISIBLE"$normal( Enemy.Location - Pawn.Location ) dot vector(Pawn.Rotation) ); Outer.Enable( 'SeePlayer' ); Outer.Disable( 'EnemyNotVisible' ); return true; } function CheckVision() { } /** Called from cheat manager */ function Debug_Vision( Pawn P ) { ClearTimer( nameof(CheckVision) ); PopState(); } } function bool NotifyHitWall( vector HitNormal, actor Wall ) { if( CachedChildCommand != None ) { `AILog( GetFuncName()$"() notifying "$CachedChildCommand$" and letting it handle the event.", 'HitWall' ); return CachedChildCommand.NotifyHitWall( HitNormal, Wall ); } return false; // else if( KFDoorActor(Wall) != none ) // { // if( KFDoorActor(Wall).WeldIntegrity <= 0 && KFDoorMarker(KFDoorActor(Wall).MyMarker) != none && !KFDoorActor(Wall).IsCompletelyOpen() ) // { // DisableNotifyHitWall(0.25f); // WaitForDoor( KFDoorActor(Wall) ); // `AILog( "NotifyHitWall() while in MoveToGoal, Wall: "$Wall$" Using door and waiting for it to open", 'Doors' ); // KFDoorActor(Wall).UseDoor(Pawn); // return false; // } // // NOTE: Unless returning true, if the Wall is a closed door, SuggestMovePreparation event will be called on the associated KFDoorMarker // `AILog( GetFuncName()$"() Wall: "$Wall$" HitNormal: "$HitNormal$" ran into a door!", 'Doors' ); // if( !KFDoorActor(Wall).IsCompletelyOpen() && KFDoorActor(Wall).WeldIntegrity > 0 && (Pawn.Anchor == KFDoorActor(Wall).MyMarker || (DoorEnemy != none && (DoorEnemy == KFDoorActor(Wall) || PendingDoor == KFDoorActor(Wall)))) ) // { // `AILog( GetFuncName()$"() calling NotifyAttackDoor for "$Wall, 'Doors' ); // NotifyAttackDoor( KFDoorActor(Wall) ); // return false; // //`AILog( GetFuncName()$"() has door enemy "$DoorEnemy, 'Doors' ); // } // } // return false; } function EnableNotifyHitWall() { bTempDisableHitWall = false; } function bool ReadyToCharge() { return false; } /********************************************************************************************* * Debug Move Forward - send pawn into latent move forward in direction pawn is facing ********************************************************************************************* */ state DebugMoveForward extends Debugging { function PushedState() { super.PushedState(); AIActionStatus = "In DebugMoveForward state"; } Ready: Steering.EnableDefaultAcceleration(); MoveTo( Pawn.Location + (vector(Pawn.Rotation) * 1024) ); Steering.DisableDefaultAcceleration(); PopState(); } /********************************************************************************************* * RotateToFocus state ********************************************************************************************* */ state RotateToFocus extends Debugging { function PushedState() { super.PushedState(); AIActionStatus = "In RotateToFocus state"; } Begin: SetFocalPoint( vect(0,0,0) ); Focus = Enemy; // Pawn.DesiredRotation = Normalize(Rotator( (Focus == None ? GetFocalPoint() : Focus.Location) - Pawn.Location )); // DesiredRotation = Pawn.DesiredRotation; FinishRotation(); SetFocalPoint(Vect(0,0,0)); Pawn.ResetDesiredRotation(); Focus = none; PopState(); } state RotateToMoveTarget extends Debugging { function PushedState() { super.PushedState(); AIActionStatus = "In RotateToFocus state"; } Begin: SetFocalPoint( vect(0,0,0) ); Focus = MoveTarget; // Pawn.DesiredRotation = Normalize(Rotator( (Focus == None ? GetFocalPoint() : Focus.Location) - Pawn.Location )); // DesiredRotation = Pawn.DesiredRotation; FinishRotation(); SetFocalPoint(Vect(0,0,0)); Pawn.ResetDesiredRotation(); Focus = none; PopState(); } /********************************************************************************************* * DebugRoaming state - NPC will roam to locations within envelope distance of player ********************************************************************************************* */ state DebugRoaming extends Debugging { function PushedState() { super.PushedState(); if( Enemy == none ) { Enemy = GetClosestEnemy(); } AIActionStatus = "In DebugRoaming state"; RoamStartTime = WorldInfo.TimeSeconds; } function Debug_Wander( optional float WanderDuraion=-1.f ) { PopState(); } function Actor GenerateRoamPath( Actor Goal, optional float Distance, optional bool bAllowPartialPath ) { local Actor NewGoal; class'Path_TowardGoal'.static.TowardGoal( Pawn, Goal ); class'Path_WithinTraversalDist'.static.DontExceedMaxDist( Pawn, MaxRoamDist, false ); // real one with soft distances class'Path_WithinDistanceEnvelope'.static.StayWithinEnvelopeToLoc( Pawn, Goal.Location, RoamEnvelopeOuter, RoamEnvelopeInner,false,, true ); // don't go somewhere we can't get out of class'Path_AvoidInEscapableNodes'.static.DontGetStuck( Pawn ); class'Goal_Null'.static.GoUntilBust( Pawn, 1024 ); NewGoal = FindPathToward( Goal ); Pawn.ClearConstraints(); return NewGoal; } function bool RoamTowardEnemy() { local actor Path; Path = GenerateRoamPath( Enemy, 100.f, true ); if( Path != none ) { // SetMoveGoal( RouteGoal,,,,true); return true; } return false; } simulated function DrawDebug( HUD HUD, name Category ) { DrawDebugText( KFHUDBase(HUD), "MaxRoamDist: "$MaxRoamDist$" RoamWaitMin: "$RoamWaitMin$" RoamWaitMax: "$RoamWaitMax ); DrawDebugText( KFHUDBase(HUD), "RoamEnvelopeInner: "$RoamEnvelopeInner$" RoamEnvelopeOuter: "$RoamEnvelopeOuter ); } Begin: AIActionStatus = "Roaming toward enemy ("$Enemy$")"; if( Enemy == none || !RoamTowardEnemy() ) { AIActionStatus = "Failed to find roam location for "$Enemy; Sleep(1.5f); Goto('Begin'); } // if we can't see our enemy from here, roam again! if( !IsPawnVisibleViaTrace(Enemy) ) { `AILog( "Enemy wasn't visible, reacquiring", 'Command_Debug' ); Sleep( 0.f ); Goto( 'Begin' ); } AIActionStatus = "Rotating toward enemy"; Focus = Enemy; SetFocalPoint( vect(0,0,0) ); Focus = MoveFocus; FinishRotation(); Sleep( RandRange(RoamWaitMin, RoamWaitMax) ); Goto( 'Begin' ); } function bool CanSeePawn( Pawn Seen ) { return true; } /********************************************************************************************* * Debugging ********************************************************************************************* */ simulated function DrawDebug( HUD HUD, name Category ) { //DrawDebugText( KFHUDBase(HUD), "Testing output from debug command" ); } defaultproperties { bAllowedToAttack=true bIgnoreNotifies=false bIgnoreStepAside=false MaxRoamDist=4096.f RoamEnvelopeOuter=1800.f RoamEnvelopeInner=750.f RoamWaitMin=0.5f RoamWaitMax=3.5f }