def input(self): controller_input = SimpleControllerState() theta = self.current_state.rot.yaw correction_vector = self.target_state.pos - self.current_state.pos facing_vector = Vec3(cos(theta), sin(theta), 0) car_to_target = (self.target_state.pos - self.current_state.pos).normalize() #Rotated to the car's reference frame on the ground. rel_correction_vector = Vec3((correction_vector.x*cos(theta)) + (correction_vector.y * sin(theta)), (-(correction_vector.x*sin(theta))) + (correction_vector.y * cos(theta)), 0) if self.can_reverse and facing_vector.dot(car_to_target) < - 0.5: correction_angle = atan2(rel_correction_vector.y, rel_correction_vector.x) controller_input.throttle = - 1.0 if abs(correction_angle) > 1.25: controller_input.handbrake = 1 controller_input.steer = cap_magnitude(-5*correction_angle, 1) else: correction_angle = atan2(rel_correction_vector.y, rel_correction_vector.x) controller_input.throttle = 1.0 if abs(correction_angle) > 1.25: controller_input.handbrake = 1 controller_input.steer = cap_magnitude(5*correction_angle, 1) return controller_input
def check_kickoff_position(current_state): ''' Returns a string encoding which starting position we have for kickoff. This only works for "mostly standard" maps. I'll worry about the others later. ''' kickoff_dict = { Vec3(0.0, -4608, 10): "Far Back", Vec3(0.0, 4608, 10): "Far Back", Vec3(-1952, -2464, 10): "Right", Vec3(1952, 2464, 10): "Right", Vec3(1952, -2464, 10): "Left", Vec3(-1952, 2464, 10): "Left", Vec3(-256.0, -3840, 10): "Back Right", Vec3(256.0, 3840, 10): "Back Right", Vec3(256.0, -3840, 10): "Back Left", Vec3(-256.0, 3840, 10): "Back Left" } #Let's figure out which kickoff it is for spot in kickoff_dict.keys(): if abs((current_state.pos - spot).magnitude()) < 500: kickoff_position = kickoff_dict[spot] break else: kickoff_position = "Other" return kickoff_position
def input(self): controller_input = SimpleControllerState() current_angle_vec = Vec3(cos(self.current_state.rot.yaw), sin(self.current_state.rot.yaw), 0) goal_angle_vec = Vec3(cos(self.goal_state.rot.yaw), sin(self.goal_state.rot.yaw), 0) vel_2d = Vec3(self.current_state.vel.x, self.current_state.vel.y, 0) if (self.current_state.pos - self.goal_state.pos).magnitude() > 400: #Turn towards target. Hold throttle until we're close enough to start stopping. controller_input = GroundTurn(self.current_state, self.goal_state).input() elif vel_2d.magnitude() < 50 and current_angle_vec.dot(goal_angle_vec) < 0: #If we're moving slowly, but not facing the right way, jump to turn in the air. #Decide which way to turn. Make sure we don't have wraparound issues. goal_x = goal_angle_vec.x goal_y = goal_angle_vec.y car_theta = self.current_state.rot.yaw #Rotated to the car's reference frame on the ground. rel_vector = Vec3((goal_x*cos(car_theta)) + (goal_y * sin(car_theta)), (-(goal_x*sin(car_theta))) + (goal_y * cos(car_theta)), 0) correction_angle = atan2(rel_vector.y, rel_vector.x) #Jump and turn to reach goal yaw. if self.current_state.wheel_contact: controller_input.jump = 1 else: controller_input.yaw = cap_magnitude(correction_angle, 1) elif self.current_state.vel.magnitude() > 400: #TODO: Proportional controller to stop in the right place controller_input.throttle = -1 else: #Wiggle to face ball #Check if the goal is ahead of or behind us, and throttle in that direction goal_angle = atan2((self.goal_state.pos - self.current_state.pos).y, (self.goal_state.pos - self.current_state.pos).x) if abs(angle_difference(goal_angle,self.current_state.rot.yaw)) > pi/2: correction_sign = -1 else: correction_sign = 1 controller_input.throttle = correction_sign #Correct as we wiggle so that we face goal_yaw. if angle_difference(self.goal_state.rot.yaw, self.current_state.rot.yaw) > 0: angle_sign = 1 else: angle_sign = -1 controller_input.steer = correction_sign*angle_sign return controller_input
def __init__(self, current_slice, team_sign): #Position, rotation, velocity, omega for the current slice self.x = current_slice.location[0] * team_sign self.y = current_slice.location[1] * team_sign self.z = current_slice.location[2] self.pos = Vec3(self.x, self.y, self.z) ''' Irrelevant for a ball. No idea if a puck will ever be supported. pitch = current_slice.pitch yaw = current_slice.yaw roll = current_slice.roll self.rot = Orientation(pyr = [ pitch, yaw, roll] ) ''' self.vx = current_slice.velocity[0] * team_sign self.vy = current_slice.velocity[1] * team_sign self.vz = current_slice.velocity[2] self.vel = Vec3(self.vx, self.vy, self.vz) self.omegax = current_slice.angular_velocity[0] * team_sign self.omegay = current_slice.angular_velocity[1] * team_sign self.omegaz = current_slice.angular_velocity[2] self.omega = Vec3(self.omegax, self.omegay, self.omegaz) self.time = current_slice.time
def offcenter(game_info=None, x_sign=None, persistent=None): controller_input = SimpleControllerState() current_state = game_info.me ball = game_info.ball team_sign = game_info.team_sign ball = game_info.ball #Set which boost we want based on team and side. if team_sign == 1: first_boost = 7 else: first_boost = 26 if abs(current_state.pos.x) > 150 and abs(current_state.pos.y) > 1100: #If we're not near the center-line of the field, boost towards the first small boost controller_input = GroundTurn( current_state, current_state.copy_state(pos=Vec3(0, -3000, 0))).input() controller_input.boost = 1 elif abs(current_state.pos.y) > 1500 and current_state.pos.z < 30: controller_input.jump = 1 controller_input.boost = 1 elif abs(current_state.pos.y) > 1000 and not current_state.double_jumped: #If we're far away, fast dodge to speed up. controller_input = CancelledFastDodge(current_state, Vec3(1, x_sign, 0)).input() elif abs(current_state.pos.y) > 355 and current_state.double_jumped: if persistent.aerial_turn.action == None: persistent.aerial_turn.initialize = True vector_to_ball = game_info.ball.pos - current_state.pos yaw_to_ball = atan2(vector_to_ball.y, vector_to_ball.x) target_rot = Orientation(pitch=pi / 3, yaw=yaw_to_ball, roll=0) persistent.aerial_turn.target_orientation = target_rot else: controller_input, persistent = aerial_rotation( game_info.dt, persistent) controller_input.boost = 1 elif abs(current_state.pos.y) > 355: controller_input = GroundTurn( current_state, current_state.copy_state(pos=Vec3(0, -team_sign * 100, 0))).input() else: controller_input = FrontDodge(current_state).input() return controller_input, persistent
def diagonal(game_info=None, x_sign=None, persistent=None): current_state = game_info.me controls = SimpleControllerState() #Set which boost we want based on team and side. if x_sign == -1: first_boost = 11 else: first_boost = 10 if game_info.boosts[first_boost].is_active: #If we haven't taken the small boost yet, drive towards it controls = GroundTurn( current_state, current_state.copy_state(pos=Vec3(0, -1000, 0))).input() controls.boost = 1 elif abs(current_state.pos.y) > 1100 and current_state.wheel_contact: controls.jump = 1 controls.boost = 1 elif abs(current_state.pos.y) > 1100 and current_state.pos.z < 40: controls.jump = 1 controls.boost = 1 elif abs(current_state.pos.y) > 500 and not current_state.double_jumped: controls = CancelledFastDodge(current_state, Vec3(1, x_sign, 0)).input() elif abs(current_state.pos.y) > 250 and not current_state.wheel_contact: if persistent.aerial_turn.action == None: persistent.aerial_turn.initialize = True target_rot = Orientation(pitch=pi / 3, yaw=current_state.rot.yaw, roll=0) persistent.aerial_turn.target_orientation = target_rot else: controls, persistent = aerial_rotation(game_info.dt, persistent) controls.boost = 1 controls.steer = x_sign #Turn into the ball elif abs(current_state.pos.y) > 235: controls.throttle = 1 controls.boost = 1 controls.steer = x_sign else: controls = FrontDodge(current_state).input() return controls, persistent
def pyr_to_matrix(pyr): pitch = pyr[0] yaw = pyr[1] roll = pyr[2] front = Vec3(cos(yaw) * cos(pitch), sin(yaw) * cos(pitch), sin(pitch)) left = Vec3(-cos(yaw) * sin(pitch) * sin(roll) - sin(yaw) * cos(roll), -sin(yaw) * sin(pitch) * sin(roll) + cos(yaw) * cos(roll), cos(pitch) * sin(roll)) up = Vec3(-cos(yaw) * sin(pitch) * cos(roll) + sin(yaw) * sin(roll), -sin(yaw) * sin(pitch) * cos(roll) - cos(yaw) * sin(roll), cos(pitch) * cos(roll)) return [front, left, up]
def goal(plan, game_info, persistent): ''' Once we've decided to go to net, this function decides if we're going to net, staying in net, etc. ''' current_state = game_info.me if game_info.ball.pos.x > 0: ball_x_sign = 1 else: ball_x_sign = -1 distance_to_net = (Vec3(0, -5120, 15) - current_state.pos).magnitude() ball_arrival = get_ball_arrival(game_info, is_ball_in_scorable_box) #TODO: Update what counts as "corner" ball_in_defensive_corner = not (game_info.ball.pos.y > -1500 or abs(game_info.ball.pos.x) < 1500) ball_in_offensive_corner = not (game_info.ball.pos.y < 950 or abs(game_info.ball.pos.x) < 1500) if distance_to_net > 500: plan.layers[1] = "Go to net" elif ball_in_defensive_corner or ball_in_offensive_corner: plan.layers[1] = "Wait in net" else: plan.layers[1] = "Wait in net" return plan, persistent
def boost(plan, game_info, persistent): ''' Decides when to break from "Boost" state. ''' current_state = game_info.me if current_state.boost > 60: #If we were going for boost, but have enough boost, go to net. plan.layers[0] = "Goal" elif plan.path != None and plan.path.finished: #TODO: Will need to be updated once it starts doing things other than sitting in net. plan.layers[0] = "Goal" elif current_state.pos.y < -1000 and plan.path != None and plan.path.waypoints == [ Vec3(0, -5120, 0) ]: #TODO: Check field info instead of hardcoding the goals everywhere? plan.layers[0] = "Goal" elif plan.path != None: plan.path_lock = True plan.layers[0] = "Boost" else: plan.layers[0] = "Boost" return plan, persistent
def get_controls(game_info, sub_state_machine): controls = SimpleControllerState() controls = CancelledFastDodge(game_info.me, Vec3(1, 1, 0)).input() persistent = game_info.persistent return controls, persistent
def diagonal(game_info, opponent_distance, x_sign): controller_input = SimpleControllerState() current_state = game_info.me ball_angle = atan2((game_info.ball.pos - current_state.pos).y, (game_info.ball.pos - current_state.pos).x) offset = Vec3(0, 0, 0) if current_state.pos.y < -2250: #Boost towards the first small boost controller_input = GroundTurn(current_state, current_state.copy_state(pos=game_info.ball.pos+offset)).input() controller_input.boost = 1 elif current_state.pos.y < - 1500: controller_input = FrontDodge(current_state).input() elif current_state.pos.y > -700: controller_input = FrontDodge(current_state).input() else: #If we're on the ground between stages, boost and turn towards the ball controller_input = GroundTurn(current_state, current_state.copy_state(pos=game_info.ball.pos)).input() if current_state.wheel_contact: controller_input.boost = 1 return controller_input
def check_far_post(pos, ball_x_sign): ''' Checks if a car position counts as "far post" when looking for teammates. x_sign marks which side of the field the ball is on, so that we know what the far post is ''' if (pos - Vec3(-ball_x_sign * 1150)).magnitude() < 500: return True return False
def car_coordinates_2d(current_state, vector): ''' Takes a Vec3 for a vector on the field and returns the same vector relative to the car ''' x = vector.x * cos(-current_state.rot.yaw) - vector.y * sin(-current_state.rot.yaw) y = vector.x * sin(-current_state.rot.yaw) + vector.y * cos(-current_state.rot.yaw) return Vec3(x,y,0)
def get_controls(game_info, sub_state_machine): controls = SimpleControllerState() controls = GroundTurn(game_info.me, game_info.me.copy_state(pos = Vec3(0, -900, 0))).input() controls.boost = 1 persistent = game_info.persistent return controls, persistent
def Car(packet, rigid_body_tick, jumped_last_frame, index, my_index, team_sign): ''' Gets the game info for a given car, and returns the values. Should be fed into a CarState object. ''' this_car = packet.game_cars[index] pos = Vec3(team_sign * this_car.physics.location.x, team_sign * this_car.physics.location.y, this_car.physics.location.z) pitch = this_car.physics.rotation.pitch yaw = this_car.physics.rotation.yaw if team_sign == -1: yaw = rotate_to_range(this_car.physics.rotation.yaw + pi, [-pi, pi]) roll = this_car.physics.rotation.roll rot = Orientation(pitch=pitch, yaw=yaw, roll=roll) vel = Vec3(team_sign * this_car.physics.velocity.x, team_sign * this_car.physics.velocity.y, this_car.physics.velocity.z) omega = Vec3(team_sign * this_car.physics.angular_velocity.x, team_sign * this_car.physics.angular_velocity.y, this_car.physics.angular_velocity.z) demo = this_car.is_demolished wheel_contact = this_car.has_wheel_contact supersonic = this_car.is_super_sonic jumped = this_car.jumped double_jumped = this_car.double_jumped boost = this_car.boost return CarState(pos=pos, rot=rot, vel=vel, omega=omega, demo=demo, wheel_contact=wheel_contact, supersonic=supersonic, jumped=jumped, double_jumped=double_jumped, boost=boost, jumped_last_frame=jumped_last_frame, index=index)
def __init__(self, current_slice, team_sign): #Position, rotation, velocity, omega for the current slice self.x = current_slice.physics.location.x * team_sign self.y = current_slice.physics.location.y * team_sign self.z = current_slice.physics.location.z self.pos = Vec3(self.x, self.y, self.z) self.vx = current_slice.physics.velocity.x * team_sign self.vy = current_slice.physics.velocity.y * team_sign self.vz = current_slice.physics.velocity.z self.vel = Vec3(self.vx, self.vy, self.vz) self.omegax = current_slice.physics.angular_velocity.x * team_sign self.omegay = current_slice.physics.angular_velocity.y * team_sign self.omegaz = current_slice.physics.angular_velocity.z self.omega = Vec3(self.omegax, self.omegay, self.omegaz) self.time = current_slice.game_seconds
def input(self): controls = SimpleControllerState() if self.me.pos.z < 40 and not self.me.double_jumped: controls.jump = 1 elif self.double_jumped: pass elif self.me.pos.z > 40: controls = AirDodge(Vec3(1, 0, 0), self.me.jumped_last_frame).input() return controls
def transition(game_info, next_states, sub_state_machine): ball_x_sign = sign(game_info.ball.pos.x) far_post_distance = (game_info.me.pos - Vec3(-893 * ball_x_sign, -5120, 0)).magnitude() ########################## def transition_to_defend(game_info): should_transition = False if game_info.me.pos.y < -4500: should_transition = True return should_transition, game_info.persistent ########################## def transition_to_transition_forward(game_info): should_transition = False return should_transition, game_info.persistent ########################## def transition_to_attack(game_info): should_transition = False return should_transition, game_info.persistent ########################## def transition_to_transition_back(game_info): should_transition = False return should_transition, game_info.persistent ########################## def transition_to_kickoff(game_info): should_transition = False if game_info.is_kickoff_pause: should_transition = True return should_transition, game_info.persistent ########################## state_transitions = [ transition_to_kickoff, transition_to_attack, transition_to_transition_back, transition_to_defend, transition_to_transition_forward ] for i in range(len(state_transitions)): should_transition, persistent = state_transitions[i](game_info) if should_transition: return next_states[i], persistent
def car_coordinates_2d(current_state, direction): ''' Takes a Vec3 for a direction on the field and returns the same direction relative to the car ''' x = direction.x * cos(-current_state.rot.yaw) - direction.y * sin( -current_state.rot.yaw) y = direction.x * sin(-current_state.rot.yaw) + direction.y * cos( -current_state.rot.yaw) return Vec3(x, y, 0)
def get_controls(game_info, sub_state_machine): ball_angle = atan2((game_info.ball.pos - game_info.me.pos).y, (game_info.ball.pos - game_info.me.pos).x) post_angle = atan2((Vec3(880, 50, 0) - game_info.me.pos).y, (Vec3(880, 50, 0) - game_info.me.pos).x) rot = Orientation( pyr=[game_info.me.rot.pitch, ball_angle, game_info.me.rot.roll]) target_state = game_info.me.copy_state(pos=Vec3(0, -5120 - 400, 0), rot=rot) controls = NavigateTo(game_info.me, target_state).input() if game_info.me.wheel_contact: if game_info.me.rot.roll > 0.15: controls.steer = 1 elif game_info.me.rot.roll < -0.15: controls.steer = -1 persistent = game_info.persistent return controls, persistent
def __init__(self): self.check = False self.action = None self.data = None self.initialize = False #Aerial Turn self.target_orientation = None #Aerial self.target_location = None self.target_time = None self.target_up = Vec3(0, 0, 1)
def get_controls(game_info, sub_state_machine): ball_angle = atan2((game_info.ball.pos - game_info.me.pos).y, (game_info.ball.pos - game_info.me.pos).x) rot = Orientation( pyr=[game_info.me.rot.pitch, ball_angle, game_info.me.rot.roll]) target_state = game_info.me.copy_state(pos=Vec3( -3072 * game_info.ball_x_sign, -4096, 0), rot=rot) controls = NavigateTo(game_info.me, target_state).input() persistent = game_info.persistent return controls, persistent
def get_controls(game_info, sub_state_machine): controls = SimpleControllerState() ball_angle = atan2((game_info.ball.pos - game_info.me.pos).y, (game_info.ball.pos - game_info.me.pos).x) rot = Orientation( pyr=[game_info.me.rot.pitch, ball_angle, game_info.me.rot.roll]) target_state = game_info.me.copy_state(pos=Vec3(0, -5120 - 80, 0), rot=rot) controls = NavigateTo(game_info.me, target_state).input() persistent = game_info.persistent return controls, persistent
def startup(game_info): state = None state_list = None target_direction = Vec3(500, 0, 0) - game_info.me.pos target_yaw = atan2(target_direction.x, target_direction.y) persistent = game_info.persistent #persistent.aerial_turn.action = RLU_AerialTurn(game_info.utils_game.my_car) target_rot = Orientation(pitch=0, yaw=target_yaw, roll=0) #persistent.aerial_turn.action.target = rot_to_mat3(target_rot, game_info.team_sign) return state, state_list, persistent
def startup(game_info): state = PathState state_list = [PathState, ChallengeState, AerialState] persistent = game_info.persistent end_tangent = Vec3(0, 1, 0) #TODO: Dynamically update end_tangent as well intercept_slice, persistent.path_follower.path, persistent.path_follower.action = ball_contact_binary_search( game_info, end_tangent=end_tangent) #persistent.path_follower.end = intercept_slice.pos return state, state_list, persistent
def is_ball_in_scorable_box(loc, vel, theta_top=pi / 6, theta_side=pi / 6, max_distance=1500): ''' Returns whether location is in the angled box in front of net of the given dimensions ''' goal_distance = loc.y + 5120 if loc.x < 0: x_sign = -1 else: x_sign = 1 near_post_vector = Vec3(x_sign * 920, -5120, 0) - Vec3(loc.x, loc.y, 0) far_post_vector = Vec3(-x_sign * 920, -5120, 0) - Vec3(loc.x, loc.y, 0) angle_to_near_post = atan2(near_post_vector.y, near_post_vector.x) angle_to_far_post = atan2(far_post_vector.y, far_post_vector.x) ball_towards_net = (x_sign * angle_to_far_post < x_sign * atan2(vel.y, vel.x) < x_sign * angle_to_near_post) if loc.y > -5120 + max_distance: #If the ball is far away from the net, it's not scorable return False elif abs(loc.x) > 893 + goal_distance * tan(theta_side): #If the ball is far to the side, the angle is too tight to score return False elif loc.z > 642.775 + goal_distance * tan(theta_top): #If the ball is too high, the angle is too tight to score return False elif not ball_towards_net and loc.z < 150: return False else: return True
def follow_waypoint(self, current_state): controller_input = SimpleControllerState() controller_input = GroundTurn( current_state, current_state.copy_state(pos=self.piece.waypoint)).input() waypoint_distance = (current_state.pos - self.waypoints[0]).magnitude() wobble = Vec3(current_state.omega.x, current_state.omega.y, 0).magnitude() epsilon = 0.3 angle_to_waypoint = atan2((self.waypoints[0] - current_state.pos).y, (self.waypoints[0] - current_state.pos).x) facing_waypoint = angles_are_close(angle_to_waypoint, current_state.rot.yaw, pi / 12) good_direction = facing_waypoint and abs( current_state.omega.z) < epsilon speed = current_state.vel.magnitude() if len(self.waypoints) > 1: angle_to_next_waypoint = atan2( (self.waypoints[1] - current_state.pos).y, (self.waypoints[0] - current_state.pos).x) facing_next_waypoint = angles_are_close(angle_to_next_waypoint, current_state.rot.yaw, pi / 6) good_direction = facing_waypoint and abs( current_state.omega.z) < epsilon and facing_next_waypoint if waypoint_distance < 400 * speed / 1410: #If we're close, start turning, and we'll hit the point through the turn. #TODO: Figure out a good path through waypoints, taking future points into account. self.waypoints = self.waypoints[1:] if len(self.waypoints) == 0: self.finished = True elif len( self.waypoints ) > 0 and 1000 < speed < 2250 and waypoint_distance > 1200 * ( speed + 500) / 1410 and wobble < epsilon and good_direction: #If we're decently far away from the next point, front flip for speed. controller_input = FrontDodge(current_state).input() elif facing_waypoint and current_state.wheel_contact and speed < 2300 and current_state.boost > 40: #If we're not supersonic and pointed the right way, boost to speed up controller_input.boost = 1 return controller_input
def __init__(self, current_state, goal_state, direction = 1, oversteer = True, boost_threshold = None): ''' direction = 1 for right, direction = -1 for left. ''' self.current_state = current_state self.goal_state = goal_state self.boost = self.current_state.boost self.direction = left_or_right(current_state, goal_state.pos) self.oversteer = oversteer #If we don't have a boost threshold, find it based on how much boost we want to use. if boost_threshold == None: self.boost_threshold = 1200 else: self.boost_threshold = boost_threshold self.accel_threshold = min(1000, self.boost_threshold) self.dodge_direction = Vec3(1/sqrt(2), self.direction * (1/sqrt(2)), 0) #questionable if we're still turning #but it should be okay if we're driving straight. if self.dodge_direction.y > 0: self.dodge_angle = atan2((goal_state.pos - current_state.pos).y, (goal_state.pos - current_state.pos).x) - (pi/10) else: self.dodge_angle = atan2((goal_state.pos - current_state.pos).y, (goal_state.pos - current_state.pos).x) + (pi/10) self.movement_angle = atan2(current_state.vel.y, current_state.vel.x) #Currently set to opposite of the dodge direction. This should be good for general use, up to oversteer. #Eventually wrap this into set_dodge_direction? if (self.dodge_angle - self.current_state.rot.yaw) >=0: self.turn_direction = 1 else: self.turn_direction = -1
def input(self): ''' Returns the controller input to perform an air dodge on the frame called, in the desired direction. ''' controller_input = SimpleControllerState() if (not self.jumped_last_frame): if (self.direction.x == self.direction.y == self.direction.z == 0): controller_input.jump = 1 return controller_input else: plane_direction = Vec3(self.direction.x, self.direction.y, 0) plane_direction_normalized = plane_direction.normalize() controller_input.jump = 1 controller_input.yaw = plane_direction_normalized.y controller_input.pitch = -plane_direction_normalized.x return controller_input
def get_controls(game_info, sub_state_machine): controls = SimpleControllerState() persistent = game_info.persistent if persistent.path_follower.action != None: persistent.path_follower.action.step(game_info.dt) controls = persistent.path_follower.action.controls else: print("No path found!") end_tangent = Vec3(0, 1, 0) #If we didn't have a path already, try to find one. Just ground turn for now, but #when we find one we'll follow it starting next tick. intercept_slice, persistent.path_follower.path, persistent.path_follower.action = ball_contact_binary_search( game_info, end_tangent=end_tangent) #persistent.path_follower.end = intercept_slice.pos controls = GroundTurn( game_info.me, game_info.me.copy_state(pos=game_info.ball.pos)).input() return controls, persistent