/* ************************************************************************* ArmageTron -- Just another Tron Lightcycle Game in 3D. Copyright (C) 2000 Manuel Moos (manuel@moosnet.de) ************************************************************************** This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *************************************************************************** */ #include "eVoter.h" #include "tMemManager.h" #include "tSysTime.h" #include "uMenu.h" #include "nConfig.h" #include "rConsole.h" #include "ePlayer.h" #include "eGrid.h" class eMenuItemVote; static unsigned short se_votingItemID = 0; static float se_votingTimeout = 300.0f; static nSettingItem< float > se_vt( "VOTING_TIMEOUT", se_votingTimeout ); static float se_votingStartDecay = 60.0f; static nSettingItem< float > se_vsd( "VOTING_START_DECAY", se_votingStartDecay ); static float se_votingDecay = 60.0f; static nSettingItem< float > se_vd( "VOTING_DECAY", se_votingDecay ); static bool se_allowVoting = false; static tSettingItem< bool > se_av( "ALLOW_VOTING", se_allowVoting ); static bool se_allowVotingSpectator = false; static tSettingItem< bool > se_avo( "ALLOW_VOTING_SPECTATOR", se_allowVotingSpectator ); static int se_minVoters = 3; static tSettingItem< int > se_mv( "MIN_VOTERS", se_minVoters ); // the number set here always acts as votes against a change. static int se_votingBias = 0; static tSettingItem< int > se_vb( "VOTING_BIAS", se_votingBias ); // voting privacy level. -2 means total disclosure, +2 total secrecy. static int se_votingPrivacy = 1; static tSettingItem< int > se_vp( "VOTING_PRIVACY", se_votingPrivacy ); static eVoter* se_GetVoter( const nMessage& m ) { return eVoter::GetVoter( m.SenderID(), true ); } // something to vote on class eVoteItem: public tListMember { friend class eMenuItemVote; public: // constructors/destructor eVoteItem( void ): creationTime_( tSysTimeFloat() ), user_( 0 ), id_( ++se_votingItemID ), menuItem_( 0 ) { items_.Add( this ); }; virtual ~eVoteItem( void ); void FillFromMessage( nMessage& m ) { // cloak the ID of the sener for privacy nCurrentSenderID cloak; if ( se_votingPrivacy > 1 ) cloak.SetID(0); DoFillFromMessage( m ); // rebroadcast message to all non-voters that may be able to vote if ( sn_GetNetState() == nSERVER ) { nMessage* ret = this->CreateMessage(); for ( int i = MAXCLIENTS; i > 0; --i ) { if ( sn_Connections[ i ].socket > 0 && i != m.SenderID() && 0 != eVoter::GetVoter( i ) ) { ret->Send( i ); } } // item->SendMessage(); } this->Evaluate(); }; nMessage* CreateMessage( void ) const { nMessage* m = tNEW( nMessage )( this->DoGetDescriptor() ); this->DoFillToMessage( *m ); return m; } void SendMessage( void ) const { this->CreateMessage()->BroadCast(); } // message sending void Vote( bool accept ); // called on the clients to accept or decline the vote static bool AcceptNewVote( nMessage& m ) // check if a new voting item should be accepted { // cloak the ID of the sener for privacy nCurrentSenderID cloak; if ( se_votingPrivacy > 0 ) cloak.SetID(0); int i; // let old messages time out for ( i = items_.Len()-1; i>=0; --i ) { items_[i]->Evaluate(); } // always accept in client mode if ( sn_GetNetState() == nCLIENT ) return true; eVoter* voter = se_GetVoter( m ); // check if voting is allowed if ( !voter ) { return false; } // reject voting if ( !se_allowVoting ) { tOutput message("$vote_disabled"); sn_ConsoleOut( message, m.SenderID() ); return false; } // spawn spectator voters for ( i = MAXCLIENTS; i > 0; --i ) { if ( sn_Connections[ i ].socket > 0 ) eVoter::GetVoter( i ); } // enough voters online? if ( eVoter::voters_.Len() < se_minVoters ) { tOutput message("$vote_toofew"); sn_ConsoleOut( message, m.SenderID() ); return false; } // check for spam if ( voter->IsSpamming( m.SenderID() ) ) { return false; } if ( items_.Len() < 5 ) { return true; } else { tOutput message("$vote_overflow"); sn_ConsoleOut( message, m.SenderID() ); return false; } } void RemoveVoter( eVoter* voter ) { // remove voter from the lists for ( int res = 1; res >= 0; --res ) this->voters_[ res ].Remove( voter ); } void RemoveVoterCompletely( eVoter* voter ) { RemoveVoter( voter ); if ( suggestor_ == voter ) { suggestor_ = 0; user_ = 0; } } // message receival static void GetControlMessage( nMessage& m ) // handles a voting message { if ( sn_GetNetState() == nSERVER ) { unsigned short id; m.Read( id ); bool result; m >> result; result = result ? 1 : 0; for ( int i = items_.Len()-1; i>=0; --i ) { eVoteItem* vote = items_[i]; if ( vote->id_ == id ) { // found the vote; find the voter tCONTROLLED_PTR( eVoter ) voter = se_GetVoter( m ); if ( voter ) { // prepare message tOutput voteMessage; voteMessage.SetTemplateParameter( 1, voter->Name( m.SenderID() ) ); voteMessage.SetTemplateParameter( 2, vote->GetDescription() ); if ( result ) voteMessage << "$vote_vote_for"; else voteMessage << "$vote_vote_against"; // print it if ( se_votingPrivacy <= -2 ) sn_ConsoleOut( voteMessage ); // broadcast it else if ( se_votingPrivacy <= 0 ) con << voteMessage; // print it for the server admin // remove him from the lists vote->RemoveVoter( voter ); // insert hum vote->voters_[ result ].Insert( voter ); } // are enough votes cast? vote->Evaluate(); return; } } } } // information void GetStats( int& pro, int& con, int& total ) const // returns voting statistics about this item { pro = voters_[1].Len(); con = voters_[0].Len(); total = eVoter::voters_.Len(); } // message void BroadcastMessage( const tOutput& message ) const { if ( sn_GetNetState() == nSERVER ) { tOutput m; m.SetTemplateParameter( 1, this->GetDescription() ); m.Append( message ); sn_ConsoleOut( m ); } } // evaluation void Evaluate() // check if this voting item is to be kept around { int pro, con, total; GetStats( pro, con, total ); // apply bias con += se_votingBias; total += se_votingBias; // reduce number of total voters if ( se_votingDecay > 0 ) { int reduce = int( ( tSysTimeFloat() - this->creationTime_ - se_votingStartDecay ) / se_votingDecay ); if ( reduce > 0 ) { total -= reduce; } } if ( sn_GetNetState() == nSERVER ) { // see if the vote has been rejected if ( con >= pro && con * 2 >= total ) { if ( this->suggestor_ ) this->suggestor_->Spam( user_ ); this->BroadcastMessage( tOutput( "$vote_rejected" ) ); delete this; return; } // see if the vote has been accepted if ( pro >= con && pro * 2 > total ) { this->BroadcastMessage( tOutput( "$vote_accepted" ) ); this->DoExecute(); delete this; return; } } // see if the voting has timed out if ( this->creationTime_ < tSysTimeFloat() - se_votingTimeout ) { this->BroadcastMessage( tOutput( "$vote_timeout" ) ); delete this; return; } } // accessors static const tList< eVoteItem >& GetItems() { return items_; } // returns the list of all items inline eVoter* GetSuggestor() const { return suggestor_; } // returns the voter that suggested the item inline tString GetDescription() const{ return this->DoGetDescription(); } // returns the description of the voting item protected: virtual void DoFillFromMessage( nMessage& m ) { // get user user_ = m.SenderID(); // get originator of vote if(sn_GetNetState()==nSERVER) { suggestor_ = se_GetVoter( m ); if ( !suggestor_ ) return; // prepare message tOutput voteMessage; voteMessage.SetTemplateParameter( 1, suggestor_->Name( user_ ) ); voteMessage.SetTemplateParameter( 2, GetDescription() ); voteMessage << "$vote_submitted"; // print it if ( se_votingPrivacy <= -1 ) sn_ConsoleOut( voteMessage ); // broadcast it else if ( se_votingPrivacy <= 1 ) con << voteMessage; // print it for the server admin // add suggestor to supporters this->voters_[1].Insert( suggestor_ ); } else { m.Read( id_ ); } tOutput newMessage; newMessage.SetTemplateParameter(1, GetDescription() ); newMessage << "$vote_new"; con << newMessage; }; virtual void DoFillToMessage( nMessage& m ) const { if(sn_GetNetState()==nSERVER) { // write our message ID m.Write( id_ ); } }; private: virtual nDescriptor& DoGetDescriptor() const = 0; // returns the creation descriptor virtual tString DoGetDescription() const = 0; // returns the description of the voting item virtual void DoExecute() = 0; // called when the voting was successful nTimeAbsolute creationTime_; // time the vote was cast tCONTROLLED_PTR( eVoter ) suggestor_; // the voter suggesting the vote unsigned int user_; // user suggesting the vote tArray< tCONTROLLED_PTR( eVoter ) > voters_[2]; // array of voters approving or disapproving of the vote static tList< eVoteItem > items_; // list of vote items unsigned short id_; // running id of voting item eMenuItemVote *menuItem_; // menu item eVoteItem& operator=( const eVoteItem& ); eVoteItem( const eVoteItem& ); }; tList< eVoteItem > eVoteItem::items_; // list of vote items static nDescriptor vote_handler(230,eVoteItem::GetControlMessage,"vote cast"); // called on the clients to accept or decline the vote void eVoteItem::Vote( bool accept ) { nMessage* m = tNEW( nMessage )( vote_handler ); *m << id_; *m << accept; m->BroadCast(); delete this; } //nDescriptor& eVoteItem::DoGetDescriptor() const; // returns the creation descriptor //tString eVoteItem::DoGetDescription() const; // returns the description of the voting item //void DoExecute(); // called when the voting was successful // ********************************************************************************************************************************* // ********************************************************************************************************************************* // something to vote on class eVoteItemKick: public eVoteItem { public: // constructors/destructor eVoteItemKick( ePlayerNetID* player = 0 ): player_( player ){}; protected: virtual void DoFillFromMessage( nMessage& m ) { // read player ID unsigned short id; m.Read(id); tJUST_CONTROLLED_PTR< ePlayerNetID > p=dynamic_cast(nNetObject::ObjectDangerous(id)); player_ = p; eVoteItem::DoFillFromMessage( m ); }; virtual void DoFillToMessage( nMessage& m ) const { if ( player_ ) m.Write( player_->ID() ); else m.Write( 0 ); eVoteItem::DoFillToMessage( m ); }; private: virtual nDescriptor& DoGetDescriptor() const; // returns the creation descriptor virtual tString DoGetDescription() const // returns the description of the voting item { tOutput output; if ( player_ ) { output.SetTemplateParameter(1, player_->name ); } else { output.SetTemplateParameter(1, tString( "XXX") ); } output << "$kick_player_text"; return output; } virtual void DoExecute() // called when the voting was successful { if ( player_ ) { int user = player_->Owner(); if ( user > 0 ) sn_KillUser( user, tOutput("$voted_kill_kick") ); } } private: nObserverPtr< ePlayerNetID > player_; // keep player referenced }; static void se_HandleKickVote( nMessage& m ) { if ( eVoteItem::AcceptNewVote( m ) ) { // accept message eVoteItem* item = tNEW( eVoteItemKick )(); item->FillFromMessage( m ); } } static nDescriptor kill_vote_handler(231,se_HandleKickVote,"Chat"); // returns the creation descriptor nDescriptor& eVoteItemKick::DoGetDescriptor() const { return kill_vote_handler; } static void se_SendKick( ePlayerNetID* p ) { eVoteItemKick kick( p ); kick.SendMessage(); } // ********************************************************************************************************************************* // ********************************************************************************************************************************* // menu item to silence selected players class eMenuItemKick: public uMenuItemAction { public: eMenuItemKick(uMenu *m, ePlayerNetID* p ) : uMenuItemAction( m, tOutput(""),tOutput("$kick_player_help" ) ) { this->name_.Clear(); this->name_.SetTemplateParameter(1, p->name ); this->name_ << "$kick_player_text"; player_ = p; } ~eMenuItemKick() { } virtual void Enter() { if(sn_GetNetState()==nSERVER) { // kill user directly sn_KillUser( player_->Owner(), tOutput("$voted_kill_kick") ); } { // issue kick vote se_SendKick( player_ ); } // leave menu to release smart pointers this->menu->Exit(); } private: tCONTROLLED_PTR( ePlayerNetID ) player_; // keep player referenced }; // ********************************************************************************************************************************* // ********************************************************************************************************************************* // voting decision enum Vote { Vote_Approve, Vote_Reject, Vote_DontMind }; #ifdef _MSC_VER #pragma warning ( disable: 4355 ) #endif // menu item to silence selected players class eMenuItemVote: public uMenuItemSelection< Vote > { friend class eVoteItem; public: eMenuItemVote(uMenu *m, eVoteItem* v ) : uMenuItemSelection< Vote >( m, tOutput(""), tOutput("$vote_help"), vote_ ) , item_( v ) , vote_ ( Vote_DontMind ) , reject_ ( *this, "$vote_reject" , "$vote_reject_help" , Vote_Reject ) , dontMind_ ( *this, "$vote_dont_mind" , "$vote_dont_mind_help" , Vote_DontMind ) , approve_ ( *this, "$vote_approve" , "$vote_approve_help" , Vote_Approve ) { tASSERT( v ); this->title.Clear(); this->title << v->GetDescription(); if ( v ) { v->menuItem_ = this; } } ~eMenuItemVote() { if ( item_ ) { item_->menuItem_ = 0; switch ( vote_ ) { case Vote_Approve: item_->Vote( true ); break; case Vote_Reject: item_->Vote( false ); break; default: break; } } } private: eVoteItem* item_; // vote item Vote vote_; // result uSelectEntry< Vote > reject_, dontMind_, approve_; // selection entries }; // ********************************************************************************************************************************* // ********************************************************************************************************************************* static nSpamProtectionSettings se_voteSpamProtection( 50.0f, tOutput("$vote_spam_protection") ); eVoter::eVoter( const tString& IP ) : IP_( IP ), votingSpam_( se_voteSpamProtection ) { voters_.Add( this ); } eVoter::~eVoter() { voters_.Remove( this ); }; void eVoter::Spam( int user ) { if ( sn_GetNetState() == nSERVER ) votingSpam_.CheckSpam( 3.0f, user ); } bool eVoter::IsSpamming( int user ) { if ( sn_GetNetState() == nSERVER ) { return nSpamProtection::Level_Ok != votingSpam_.CheckSpam( 0.0f, user ); } return false; } void eVoter::RemoveFromGame() { tCONTROLLED_PTR( eVoter ) keeper( this ); voters_.Remove( this ); // remove from items for ( int i = eVoteItem::GetItems().Len()-1; i>=0; --i ) { eVoteItem::GetItems()( i )->RemoveVoterCompletely( this ); } } void eVoter::KickMenu() // activate player kick menu { uMenu menu( "$player_police_kick_text" ); int size = se_PlayerNetIDs.Len(); eMenuItemKick** items = tNEW( eMenuItemKick* )[ size ]; int i; for ( i = size-1; i>=0; --i ) { ePlayerNetID* player = se_PlayerNetIDs[ i ]; if ( player->IsHuman() ) { items[i] = tNEW( eMenuItemKick )( &menu, player ); } else { items[i] = 0; } } menu.Enter(); for ( i = size - 1; i>=0; --i ) { if( items[i] ) delete items[i]; } delete[] items; } #ifndef DEDICATED static bool se_KeepConsoleSmall() { return true; } #endif static uMenu* votingMenu = 0; void eVoter::VotingMenu() // activate voting menu ( you can vote about suggestions there ) { static bool recursion = false; if ( ! recursion ) { // expire old items if ( !VotingPossible() ) return; #ifndef DEDICATED rSmallConsoleCallback SmallConsole( se_KeepConsoleSmall ); // count items int size = eVoteItem::GetItems().Len(); if ( size == 0 ) return; // fill menu uMenu menu( "$voting_menu_text" ); eMenuItemVote** items = tNEW( eMenuItemVote* )[ size ]; int i; for ( i = size-1; i>=0; --i ) { items[i] = tNEW( eMenuItemVote )( &menu, eVoteItem::GetItems()( i ) ); } // enter menu recursion = true; votingMenu = &menu; menu.Enter(); votingMenu = 0; recursion = false; for ( i = size - 1; i>=0; --i ) { delete items[i]; } delete[] items; // expire old items VotingPossible(); #endif } } bool eVoter::VotingPossible() { // expire old items for ( int i = eVoteItem::GetItems().Len()-1; i>=0; --i ) { eVoteItem::GetItems()( i )->Evaluate(); } if ( sn_GetNetState() != nCLIENT ) { return false; } return eVoteItem::GetItems().Len() > 0; } eVoter* eVoter::GetVoter( int ID, bool complain ) // find or create the voter for the specified ID { // see if there is a player on the specified ID if ( !se_allowVotingSpectator ) { bool player = false; for ( int i = se_PlayerNetIDs.Len()-1; i>=0; --i ) { ePlayerNetID* p = se_PlayerNetIDs(i); if ( p->Owner() == ID ) player = true; } if (!player) { if ( complain ) { tOutput message("$vote_disabled_spectator"); sn_ConsoleOut( message, ID ); } return NULL; } } // get IP from network subsystem tString IP; sn_GetAdr( ID, IP ); #ifdef DEBUG // add client ID so multiple connects from one machine are distinquished tString newIP; newIP << ID << " " << IP; IP = newIP; #endif return eVoter::GetVoterFromIP( IP ); } eVoter* eVoter::GetVoterFromIP( tString IP ) // find or create the voter for the specified IP { // strip port for ( int pos = IP.Len()-1; pos >=0; --pos ) { if ( IP[pos] == ':' ) { IP[pos] = 0; IP.SetLen( pos ); } } // find IP for ( int i = voters_.Len()-1; i >= 0; --i ) { eVoter* voter = voters_[i]; if ( voter->IP_ == IP ) { return voter; } } return tNEW( eVoter )( IP ); } tList< eVoter > eVoter::voters_; // list of all voters static void se_Cleanup() { if ( nCallbackLoginLogout::User() == 0 ) { if ( votingMenu ) { votingMenu->Exit(); } if ( !nCallbackLoginLogout::Login() && eGrid::CurrentGrid() ) { // uMenu::exitToMain = true; } // client login/logout: delete voting items const tList< eVoteItem >& list = eVoteItem::GetItems(); while ( list.Len() > 0 ) { delete list(0); } } else if ( nCallbackLoginLogout::Login() ) { // new user: send pending voting items const tList< eVoteItem >& list = eVoteItem::GetItems(); for ( int i = list.Len()-1; i >= 0; -- i) { eVoteItem* vote = list( i ); nMessage* m = vote->CreateMessage(); m->Send( nCallbackLoginLogout::User() ); } } } static nCallbackLoginLogout se_cleanup( se_Cleanup ); eVoteItem::~eVoteItem( void ) { items_.Remove( this ); if ( menuItem_ ) { menuItem_->item_ = 0; } }; // ********************************************************************************************************** // * // * Name // * // ********************************************************************************************************** //! //! @param senderID the ID of the network user ( default: take any ) //! @return the name of the voter ( all players of that IP ) //! // ********************************************************************************************************** tString eVoter::Name( int senderID ) const { tString name; // collect the names of all players associated with this voter for ( int i = se_PlayerNetIDs.Len()-1; i>=0; --i ) { ePlayerNetID* p = se_PlayerNetIDs(i); if ( eVoter::GetVoter( p->Owner() ) == this && ( senderID < 0 || p->Owner() == senderID ) ) { if ( name.Len() > 1 ) name << ", "; name << p->name; } } if ( name.Len() < 2 ) name = IP_; return name; }