Commit 59c57e88 authored by drallensmith's avatar drallensmith

Try at putting action_status into state vector

parent 5df1cd17
......@@ -114,8 +114,6 @@ hfo_lib.getNumTeammates.argtypes = [c_void_p]
hfo_lib.getNumTeammates.restype = c_int
hfo_lib.getNumOpponents.argtypes = [c_void_p]
hfo_lib.getNumOpponents.restype = c_int
hfo_lib.getLastActionStatus.argtypes = [c_void_p, c_int]
hfo_lib.getLastActionStatus.restype = c_int
class HFOEnvironment(object):
def __init__(self):
......@@ -207,16 +205,6 @@ class HFOEnvironment(object):
""" Returns the number of opponents of the agent """
return hfo_lib.getNumOpponents(self.obj)
def getLastActionStatus(self, last_action):
"""
If last_action is the last action with a recorded status,
returns ACTION_STATUS_MAYBE for possible success,
ACTION_STATUS_BAD for no possibility of success,
or ACTION_STATUS_UNKNOWN if unknown. If it is not the
last action with a recorded status, returns ACTION_STATUS_UNKNOWN.
"""
return hfo_lib.getLastActionStatus(self.obj, last_action)
def actionStatusToString(self, status):
"""Returns a string representation of an action status."""
return ACTION_STATUS_STRINGS[status]
......@@ -123,10 +123,6 @@ int HFOEnvironment::getNumOpponents() {
return agent->getNumOpponents();
}
action_status_t HFOEnvironment::getLastActionStatus(action_t last_action) {
return agent->getLastActionStatus(last_action);
}
Player HFOEnvironment::playerOnBall() {
return agent->getPlayerOnBall();
}
......
......@@ -55,8 +55,6 @@ class HFOEnvironment {
// Returns the number of opponents
virtual int getNumOpponents();
virtual action_status_t getLastActionStatus(action_t last_action);
// Get the current player holding the ball
virtual Player playerOnBall();
......
......@@ -151,9 +151,8 @@ Agent::Agent()
// set communication planner
M_communication = Communication::Ptr(new SampleCommunication());
// setup last_action variables
last_action_with_status = NOOP;
last_action_status = ACTION_STATUS_UNKNOWN;
// setup last_action variable
last_action_status = false;
}
Agent::~Agent() {
......@@ -166,27 +165,11 @@ int Agent::getUnum() {
return world().self().unum();
}
action_status_t Agent::getLastActionStatus(action_t last_action) {
if (last_action == last_action_with_status) {
return last_action_status;
void Agent::setLastActionStatusCollision(bool may_fix, bool likely_success) {
if (likely_success || may_fix) {
last_action_status = true;
} else {
return ACTION_STATUS_UNKNOWN;
}
}
void Agent::addLastActionStatus(action_t last_action, action_status_t action_status) {
last_action_with_status = last_action;
last_action_status = action_status;
}
void Agent::addLastActionStatusCollision(action_t last_action, bool may_fix, bool likely_success) {
last_action_with_status = last_action;
if (likely_success) {
last_action_status = ACTION_STATUS_MAYBE;
} else if (may_fix) {
last_action_status = ACTION_STATUS_UNKNOWN;
} else {
last_action_status = ACTION_STATUS_BAD;
last_action_status = false;
}
}
......@@ -287,28 +270,27 @@ void Agent::actionImpl() {
switch(requested_action) {
case DASH:
addLastActionStatusCollision(DASH, may_fix, this->doDash(params[0], params[1]));
setLastActionStatusCollision(may_fix, this->doDash(params[0], params[1]));
break;
case TURN:
addLastActionStatusCollision(TURN, may_fix, this->doTurn(params[0]));
setLastActionStatusCollision(may_fix, this->doTurn(params[0]));
break;
case TACKLE:
addLastActionStatus(TACKLE, BooleanToActionStatus(this->doTackle(params[0], false)));
setLastActionStatus(this->doTackle(params[0], false));
break;
case KICK:
addLastActionStatus(KICK, BooleanToActionStatus(this->doKick(params[0], params[1])));
setLastActionStatus(this->doKick(params[0], params[1]));
break;
case KICK_TO:
if (feature_extractor != NULL) {
addLastActionStatus(KICK_TO,
BooleanToActionStatus(Body_SmartKick(Vector2D(feature_extractor->absoluteXPos(params[0]),
setLastActionStatus(Body_SmartKick(Vector2D(feature_extractor->absoluteXPos(params[0]),
feature_extractor->absoluteYPos(params[1])),
params[2], params[2] * 0.99, 3).execute(this)));
params[2], params[2] * 0.99, 3).execute(this));
}
break;
case MOVE_TO:
if (feature_extractor != NULL) {
addLastActionStatusCollision(MOVE_TO, may_fix,
setLastActionStatusCollision(may_fix,
Body_GoToPoint(Vector2D(feature_extractor->absoluteXPos(params[0]),
feature_extractor->absoluteYPos(params[1])), 0.25,
ServerParam::i().maxDashPower()).execute(this));
......@@ -316,50 +298,51 @@ void Agent::actionImpl() {
break;
case DRIBBLE_TO:
if (feature_extractor != NULL) {
addLastActionStatusCollision(DRIBBLE_TO, may_fix,
setLastActionStatusCollision(may_fix,
Body_Dribble(Vector2D(feature_extractor->absoluteXPos(params[0]),
feature_extractor->absoluteYPos(params[1])), 1.0,
ServerParam::i().maxDashPower(), 2).execute(this));
}
break;
case INTERCEPT:
addLastActionStatusCollision(INTERCEPT, may_fix, Body_Intercept().execute(this));
setLastActionStatusCollision(may_fix, Body_Intercept().execute(this));
break;
case MOVE:
addLastActionStatus(MOVE, this->doMove());
setLastActionStatus(this->doMove());
break;
case SHOOT:
addLastActionStatus(SHOOT, this->doSmartKick());
setLastActionStatus(this->doSmartKick());
break;
case PASS:
addLastActionStatus(PASS, this->doPassTo(int(params[0])));
setLastActionStatus(this->doPassTo(int(params[0])));
break;
case DRIBBLE:
addLastActionStatus(DRIBBLE, this->doDribble());
setLastActionStatus(this->doDribble());
break;
case CATCH:
addLastActionStatus(CATCH, BooleanToActionStatus(this->doCatch()));
setLastActionStatus(this->doCatch());
break;
case NOOP:
setLastActionStatus(false);
break;
case QUIT:
std::cout << "Got quit from agent." << std::endl;
handleExit();
return;
case REDUCE_ANGLE_TO_GOAL:
addLastActionStatus(REDUCE_ANGLE_TO_GOAL, this->doReduceAngleToGoal());
setLastActionStatus(this->doReduceAngleToGoal());
break;
case MARK_PLAYER:
addLastActionStatus(MARK_PLAYER, this->doMarkPlayer(int(params[0])));
setLastActionStatus(this->doMarkPlayer(int(params[0])));
break;
case DEFEND_GOAL:
addLastActionStatus(DEFEND_GOAL, this->doDefendGoal());
setLastActionStatus(this->doDefendGoal());
break;
case GO_TO_BALL:
addLastActionStatus(GO_TO_BALL, this->doGoToBall());
setLastActionStatus(this->doGoToBall());
break;
case REORIENT:
addLastActionStatus(REORIENT, this->doReorient());
setLastActionStatus(this->doReorient());
break;
default:
std::cerr << "ERROR: Unsupported Action: "
......@@ -745,7 +728,7 @@ Agent::doPreprocess()
Alternative high-level action to always doing "Move"; usable by either side, although
probably more useful for offense. Variant of doPreprocess (above), which is called by doDribble.
*/
action_status_t
bool
Agent::doReorient()
{
// check tackle expires
......@@ -767,11 +750,8 @@ Agent::doReorient()
__FILE__": tackle wait. expires= %d",
wm.self().tackleExpires() );
if (Bhv_Emergency().execute( this )) { // includes change view
return ACTION_STATUS_MAYBE;
} else {
return ACTION_STATUS_UNKNOWN;
}
Bhv_Emergency().execute( this ); // includes change view
return true;
}
//
......@@ -785,7 +765,7 @@ Agent::doReorient()
Vector2D move_point = Strategy::i().getPosition( wm.self().unum() );
Bhv_CustomBeforeKickOff( move_point ).execute( this );
this->setViewAction( new View_Tactical() );
return ACTION_STATUS_MAYBE;
return true;
}
//
......@@ -800,11 +780,8 @@ Agent::doReorient()
dlog.addText( Logger::TEAM,
__FILE__": invalid my vel" );
}
if (Bhv_Emergency().execute( this )) { // includes change view
return ACTION_STATUS_MAYBE;
} else {
return ACTION_STATUS_UNKNOWN;
}
Bhv_Emergency().execute( this ); // includes change view
return true;
}
//
......@@ -826,11 +803,8 @@ Agent::doReorient()
dlog.addText( Logger::TEAM,
__FILE__": search ball" );
if (Bhv_NeckBodyToBall().execute( this )) {
return ACTION_STATUS_MAYBE;
} else {
return ACTION_STATUS_UNKNOWN;
}
Bhv_NeckBodyToBall().execute( this );
return true;
}
......@@ -839,7 +813,7 @@ Agent::doReorient()
//
if ( doHeardPassReceive() )
{
return ACTION_STATUS_MAYBE;
return true;
}
const BallObject& ball = wm.ball();
......@@ -847,11 +821,8 @@ Agent::doReorient()
dlog.addText( Logger::TEAM,
__FILE__": search ball" );
if (Bhv_NeckBodyToBall().execute( this )) {
return ACTION_STATUS_MAYBE;
} else {
return ACTION_STATUS_UNKNOWN;
}
Bhv_NeckBodyToBall().execute( this );
return true;
}
//
......@@ -861,17 +832,17 @@ Agent::doReorient()
{
dlog.addText( Logger::TEAM,
__FILE__": do queued intention" );
return ACTION_STATUS_MAYBE;
return true;
}
return ACTION_STATUS_BAD;
return false;
}
/*-------------------------------------------------------------------*/
/*!
*/
action_status_t
bool
Agent::doShoot()
{
const WorldModel & wm = this->world();
......@@ -886,23 +857,23 @@ Agent::doShoot()
// reset intention
this->setIntention( static_cast< SoccerIntention * >( 0 ) );
return ACTION_STATUS_MAYBE;
return true;
}
return ACTION_STATUS_BAD;
return false;
}
action_status_t
bool
Agent::doSmartKick()
{
const ShootGenerator::Container & cont =
ShootGenerator::instance().courses(this->world(), false);
ShootGenerator::Container::const_iterator best_shoot
= std::min_element(cont.begin(), cont.end(), ShootGenerator::ScoreCmp());
return BooleanToActionStatus(Body_SmartKick(best_shoot->target_point_,
return Body_SmartKick(best_shoot->target_point_,
best_shoot->first_ball_speed_,
best_shoot->first_ball_speed_ * 0.99,
3).execute(this));
3).execute(this);
}
......@@ -919,19 +890,19 @@ Agent::doPass()
return true;
}
action_status_t
bool
Agent::doPassTo(int receiver)
{
Force_Pass pass;
pass.get_pass_to_player(this->world(), receiver);
return BooleanToActionStatus(pass.execute(this));
return pass.execute(this);
}
/*-------------------------------------------------------------------*/
/*!
*/
action_status_t
bool
Agent::doDribble()
{
bool success = false;
......@@ -945,9 +916,9 @@ Agent::doDribble()
success = doPreprocess();
ActionChainHolder::instance().update( world() );
if (Bhv_ChainAction(ActionChainHolder::instance().graph()).execute(this)) {
return ACTION_STATUS_MAYBE;
return true;
} else {
return BooleanToActionStatus(success);
return success;
}
}
......@@ -955,19 +926,19 @@ Agent::doDribble()
/*!
*/
action_status_t
bool
Agent::doMove()
{
Strategy::instance().update( world() );
int role_num = Strategy::i().roleNumber(world().self().unum()); // Unused?
return Bhv_BasicMove().action_execute(this);
return Bhv_BasicMove().execute(this);
}
/*-------------------------------------------------------------------*/
/*!
* This Action marks the player with the specified uniform number.
*/
action_status_t Agent::doMarkPlayer(int unum) {
bool Agent::doMarkPlayer(int unum) {
const WorldModel & wm = this->world();
Vector2D kicker_pos = Vector2D :: INVALIDATED;
Vector2D player_pos = Vector2D :: INVALIDATED;
......@@ -991,26 +962,24 @@ action_status_t Agent::doMarkPlayer(int unum) {
if (!player_pos.isValid()) {
//Player to be marked not found
return ACTION_STATUS_BAD;
return false;
}
if (!kicker_pos.isValid()) {
//Kicker not found
return ACTION_STATUS_BAD;
return false;
}
if (unum == kicker_unum || kicker_pos.equals(player_pos)) {
//Player to be marked is kicker
return ACTION_STATUS_BAD;
return false;
}
double x = player_pos.x + (kicker_pos.x - player_pos.x)*0.1;
double y = player_pos.y + (kicker_pos.y - player_pos.y)*0.1;
bool may_fix = wm.self().collidesWithPost();
if (Body_GoToPoint(Vector2D(x,y), 0.25, ServerParam::i().maxDashPower()).execute(this)) {
return ACTION_STATUS_MAYBE;
} else if (may_fix) {
return ACTION_STATUS_UNKNOWN;
return true;
} else {
return ACTION_STATUS_BAD;
return may_fix;
}
}
......@@ -1020,12 +989,12 @@ action_status_t Agent::doMarkPlayer(int unum) {
* This action cuts off the angle between the shooter and the goal the players always move to a dynamic line in between the kicker and the goal.
*/
/* Comparator for sorting teammated based on y positions.*/
/* Comparator for sorting teammates based on y positions.*/
bool compare_y_pos (PlayerObject* i, PlayerObject* j) {
return i->pos().y < j->pos().y;
}
action_status_t Agent::doReduceAngleToGoal() {
bool Agent::doReduceAngleToGoal() {
const WorldModel & wm = this->world();
Vector2D goal_pos1( -ServerParam::i().pitchHalfLength(), ServerParam::i().goalHalfWidth() );
Vector2D goal_pos2( -ServerParam::i().pitchHalfLength(), -ServerParam::i().goalHalfWidth() );
......@@ -1034,7 +1003,7 @@ action_status_t Agent::doReduceAngleToGoal() {
const BallObject& ball = wm.ball();
if (! ball.rposValid()) {
return ACTION_STATUS_BAD;
return false;
}
Vector2D ball_pos = ball.pos();
......@@ -1105,11 +1074,9 @@ action_status_t Agent::doReduceAngleToGoal() {
Vector2D target = targetLineEnd1 * ratio + targetLineEnd2 * (1-ratio);
bool may_fix = wm.self().collidesWithPost();
if (Body_GoToPoint(target, 0.25, ServerParam::i().maxDashPower()).execute(this)) {
return ACTION_STATUS_MAYBE;
} else if (may_fix) {
return ACTION_STATUS_UNKNOWN;
return true;
} else {
return ACTION_STATUS_BAD;
return may_fix;
}
}
......@@ -1120,7 +1087,7 @@ action_status_t Agent::doReduceAngleToGoal() {
* This action cuts off the angle between the shooter and the goal; the player always moves on a fixed line.
*/
action_status_t Agent::doDefendGoal() {
bool Agent::doDefendGoal() {
const WorldModel & wm = this->world();
Vector2D goal_pos1( -ServerParam::i().pitchHalfLength() + ServerParam::i().goalAreaLength(), ServerParam::i().goalHalfWidth() );
Vector2D goal_pos2( -ServerParam::i().pitchHalfLength() + ServerParam::i().goalAreaLength(), -ServerParam::i().goalHalfWidth() );
......@@ -1137,11 +1104,9 @@ action_status_t Agent::doDefendGoal() {
bool may_fix = wm.self().collidesWithPost();
if (Body_GoToPoint(target, 0.25, ServerParam::i().maxDashPower()).execute(this)) {
return ACTION_STATUS_MAYBE;
} else if (may_fix) {
return ACTION_STATUS_UNKNOWN;
return true;
} else {
return ACTION_STATUS_BAD;
return may_fix;
}
}
......@@ -1153,13 +1118,13 @@ action_status_t Agent::doDefendGoal() {
*/
action_status_t Agent::doGoToBall() {
bool Agent::doGoToBall() {
const WorldModel & wm = this->world();
const BallObject& ball = wm.ball();
if (! ball.rposValid()) {
return ACTION_STATUS_BAD;
return false;
}
return BooleanToActionStatus(Body_GoToPoint(ball.pos(), 0.25, ServerParam::i().maxDashPower()).execute(this));
return Body_GoToPoint(ball.pos(), 0.25, ServerParam::i().maxDashPower()).execute(this);
}
/*-------------------------------------------------------------------*/
......
......@@ -65,8 +65,7 @@ protected:
std::vector<float> params; // Parameters of current action
int num_teammates; // Number of teammates
int num_opponents; // Number of opponents
hfo::action_t last_action_with_status; // Last action with a recorded return status
hfo::action_status_t last_action_status; // Recorded return status of last action
bool last_action_status; // Recorded return status of last action
public:
inline const std::vector<float>& getState() { return state; }
......@@ -76,7 +75,7 @@ protected:
int getUnum(); // Returns the uniform number of the player
inline int getNumTeammates() { return num_teammates; }
inline int getNumOpponents() { return num_opponents; }
hfo::action_status_t getLastActionStatus(hfo::action_t last_action); // if last_action is correct, returns status if available
inline bool getLastActionStatus() { return last_action_status; }
inline void setFeatureSet(hfo::feature_set_t fset) { feature_set = fset; }
inline std::vector<float>* mutable_params() { return &params; }
......@@ -85,23 +84,23 @@ protected:
private:
bool doPreprocess();
hfo::action_status_t doReorient();
hfo::action_status_t doSmartKick();
hfo::action_status_t doShoot();
bool doReorient();
bool doSmartKick();
bool doShoot();
bool doPass();
hfo::action_status_t doPassTo(int receiver);
hfo::action_status_t doDribble();
hfo::action_status_t doMove();
bool doPassTo(int receiver);
bool doDribble();
bool doMove();
bool doForceKick();
bool doHeardPassReceive();
hfo::action_status_t doMarkPlayer(int unum);
bool doMarkPlayer(int unum);
bool doMarkPlayerNearIndex(int near_index);
hfo::action_status_t doReduceAngleToGoal();
hfo::action_status_t doDefendGoal();
hfo::action_status_t doGoToBall();
bool doReduceAngleToGoal();
bool doDefendGoal();
bool doGoToBall();
bool doNewAction1();
void addLastActionStatus(hfo::action_t last_action, hfo::action_status_t action_status);
void addLastActionStatusCollision(hfo::action_t last_action, bool may_fix, bool likely_success);
inline void setLastActionStatus(bool likely_success) { last_action_status = likely_success; }
void setLastActionStatusCollision(bool may_fix, bool likely_success);
Communication::Ptr M_communication;
......
......@@ -52,32 +52,23 @@
using namespace rcsc;
bool
Bhv_BasicMove::execute( PlayerAgent * agent )
{
if (Bhv_BasicMove::action_execute(agent) == hfo::ACTION_STATUS_MAYBE) {
return true;
} else {
return false;
}
}
/*-------------------------------------------------------------------*/
/*!
*/
hfo::action_status_t
bool
Bhv_BasicMove::action_execute( PlayerAgent * agent )
{
dlog.addText( Logger::TEAM,
__FILE__": Bhv_BasicMove" );
hfo::action_status_t success = hfo::ACTION_STATUS_UNKNOWN;
bool success = false;
bool maybe_success = true;
//-----------------------------------------------
// tackle
if ( Bhv_BasicTackle( 0.8, 80.0 ).execute( agent ) )
{
return hfo::ACTION_STATUS_MAYBE;
return true;
}
const WorldModel & wm = agent->world();
......@@ -96,7 +87,7 @@ Bhv_BasicMove::action_execute( PlayerAgent * agent )
{
dlog.addText( Logger::TEAM,
__FILE__": intercept" );
success = hfo::BooleanToActionStatus(Body_Intercept().execute( agent ));
success = Body_Intercept().execute( agent );
agent->setNeckAction( new Neck_OffensiveInterceptNeck() );
return success;
}
......@@ -107,7 +98,7 @@ Bhv_BasicMove::action_execute( PlayerAgent * agent )
const BallObject& ball = wm.ball();
if (! ball.rposValid()) {
if (! wm.self().collidesWithPost()) {
success = hfo::ACTION_STATUS_BAD;
maybe_success = false;
}
}
......@@ -127,10 +118,12 @@ Bhv_BasicMove::action_execute( PlayerAgent * agent )
).execute( agent ) )
{
if (! Body_TurnToBall().execute( agent )) {
success = hfo::ACTION_STATUS_BAD;
success = false;
} else {
success = maybe_success;
}
} else if (success != hfo::ACTION_STATUS_BAD) {
success = hfo::ACTION_STATUS_MAYBE;
} else {
success = maybe_success;
}
if ( wm.existKickableOpponent() &&
......
......@@ -38,7 +38,6 @@ public:
{ }
bool execute( rcsc::PlayerAgent * agent );
hfo::action_status_t action_execute( rcsc::PlayerAgent * agent );
private:
double getDashPower( const rcsc::PlayerAgent * agent );
......
......@@ -53,24 +53,6 @@ enum status_t
SERVER_DOWN // Server is not alive
};
// Action status
enum action_status_t {
ACTION_STATUS_UNKNOWN = -1, // cannot tell or invalid action # in status request
ACTION_STATUS_BAD = 0, // definitely not OK
ACTION_STATUS_MAYBE = 1, // may be OK, may not
};
/**
* Translates from boolean false (bad) or true (maybe OK) to action status
*/
inline action_status_t BooleanToActionStatus(const bool status) {
if (status) {
return ACTION_STATUS_MAYBE;
} else{
return ACTION_STATUS_BAD;
}
}
// Configuration of the HFO domain including the team names and player
// numbers for each team. This struct is populated by ParseConfig().
struct Config {
......
// -*-c++-*-
#ifndef FEATURE_EXTRACTOR_H
#define FEATURE_EXTRACTOR_H
......
......@@ -4,6 +4,7 @@
#include "highlevel_feature_extractor.h"
#include <rcsc/common/server_param.h>
#include "agent.h"
using namespace rcsc;
......@@ -16,6 +17,7 @@ HighLevelFeatureExtractor::HighLevelFeatureExtractor(int num_teammates,
assert(numOpponents >= 0);
numFeatures = num_basic_features + features_per_teammate * numTeammates
+ features_per_opponent * numOpponents;
numFeatures++; // action status
feature_vec.resize(numFeatures);
}
......@@ -178,6 +180,12 @@ const std::vector<float>& HighLevelFeatureExtractor::ExtractFeatures(
addFeature(FEAT_INVALID);
}
if (getLastActionStatus()) {
addFeature(FEAT_MAX);
} else {
addFeature(FEAT_MIN);
}
assert(featIndx == numFeatures);
// checkFeatures();
return feature_vec;
......
// -*-c++-*-
#ifndef HIGHLEVEL_FEATURE_EXTRACTOR_H
#define HIGHLEVEL_FEATURE_EXTRACTOR_H
......
......@@ -4,6 +4,7 @@
#include "lowlevel_feature_extractor.h"
#include <rcsc/common/server_param.h>
#include "agent.h"
using namespace rcsc;
......@@ -17,6 +18,7 @@ LowLevelFeatureExtractor::LowLevelFeatureExtractor(int num_teammates,
numFeatures = num_basic_features +
features_per_player * (numTeammates + numOpponents);
numFeatures += numTeammates + numOpponents; // Uniform numbers
numFeatures++; // action state
feature_vec.resize(numFeatures);
}
......@@ -197,7 +199,7 @@ const std::vector<float>& LowLevelFeatureExtractor::ExtractFeatures(
detected_teammates++;
}
}
// Add -2 features for any missing teammates
// Add -1 features for any missing teammates
for (int i=detected_teammates; i<numTeammates; ++i) {
addFeature(FEAT_MIN);
}
......@@ -212,11 +214,17 @@ const std::vector<float>& LowLevelFeatureExtractor::ExtractFeatures(
detected_opponents++;
}
}
// Add -2 features for any missing opponents
// Add -1 features for any missing opponents
for (int i=detected_opponents; i<numOpponents; ++i) {
addFeature(FEAT_MIN);
}
if (getLastActionStatus()) {
addFeature(FEAT_MAX);
} else {
addFeature(FEAT_MIN);
}
assert(featIndx == numFeatures);
checkFeatures();
return feature_vec;
......
// -*-c++-*-
#ifndef LOWLEVEL_FEATURE_EXTRACTOR_H
#define LOWLEVEL_FEATURE_EXTRACTOR_H
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment