[SCRIPT] Votekick

Intended for use on live public servers.
6 posts Page 1 of 1
thepolm3
Scripter
Scripter
Posts: 424
Joined: Sat Feb 16, 2013 10:49 pm


Hello, here I have a short enhancement to the "votekick" script- there have been many complaints as to the worthwileness of such a system, And i believe I have the answer;
The following is both an improvement on the origiinal, and my original, script.This allows for both /y and /n to be usedd, simplifying the votekick process and allowing people who genuinely hack to be kicked, while stupid votekicks can be voted off.
In tests this newer system has worked well, but I would like to see if it appeals to the wider community;
Please try this votekick today! Green_BigSmile
Code: Select all
# maintained by triplefox

# Copyright (c) James Hofmann 2012.

# This file is part of pyspades.

# pyspades 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 3 of the License, or
# (at your option) any later version.

# pyspades 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 pyspades.  If not, see <http://www.gnu.org/licenses/>.

from twisted.internet.reactor import seconds
from scheduler import Scheduler
from commands import name, add, admin, alias, get_player, join_arguments, InvalidPlayer

REQUIRE_REASON = False
INVALID_REASONS = ["noob","fail","lag"]

S_INVALID_REASON = 'You cannot start a voteckick for {reason}'
S_NO_VOTEKICK = 'No votekick in progress'
S_DEFAULT_REASON = 'NO REASON GIVEN'
S_IN_PROGRESS = 'Votekick already in progress'
S_SELF_VOTEKICK = "You can't votekick yourself"
S_NOT_ENOUGH_PLAYERS = "There aren't enough players to vote"
S_VOTEKICK_IMMUNE = "You can't votekick this player"
S_NOT_YET = "You can't start another votekick yet!"
S_NEED_REASON = 'You must provide a reason for the votekick'
S_CANT_CANCEL = "You didn't start the votekick!"
S_NO = '{player} voted NO'
S_YES = '{player} voted YES'
S_CHANGED_NO = '{player} changed his vote to NO'
S_CHANGED_YES = '{player} changed his vote to YES'
S_ENDED = 'Votekick for {victim} has ended. {result}'
S_RESULT_TIMED_OUT = 'Votekick timed out.'
S_RESULT_CANCELLED = 'Cancelled'
S_RESULT_BANNED = 'Banned by admin'
S_RESULT_KICKED = 'Kicked by admin'
S_RESULT_INSTIGATOR_KICKED = 'Instigator kicked by admin'
S_RESULT_LEFT = '{victim} left during votekick'
S_RESULT_INSTIGATOR_LEFT = 'Instigator {instigator} left'
S_RESULT_FAILED = 'Player not kicked'
S_RESULT_PASSED = 'Player kicked'
S_ANNOUNCE_IRC = '* {instigator} started a votekick against player {victim}. ' \
    'Reason: {reason}'
S_ANNOUNCE = '{instigator} started a VOTEKICK against {victim}. Say /n or /y to vote'
S_ANNOUNCE_SELF = 'You started a votekick against {victim}. Say /CANCEL to ' \
    'stop it'
S_UPDATE = '{instigator} is votekicking {victim}. Use /N or /Y. Currently {tnot}kicking ({votes}) {duration}s '
S_REASON = 'Reason: {reason}'
S_VICTIM_VOTES = "You cannot vote in your own votekick!"
S_ALREADY_VOTED = "You have already voted for this"

class VotekickFailure(Exception):
    pass

@name('votekick')
def start_votekick(connection, *args):
    protocol = connection.protocol
    if connection not in protocol.players:
        raise KeyError()
    player = connection

    if not args:
        if protocol.votekick:
            # player requested votekick info
            protocol.votekick.send_chat_update(player)
            return
        raise ValueError()
    
    value = args[0]
    try:
        # vanilla aos behavior
        victim = get_player(protocol, '#' + value)
    except InvalidPlayer:
        victim = get_player(protocol, value)
    reason = join_arguments(args[1:])

    for word in INVALID_REASONS:
        if word in reason.lower():
            return S_INVALID_REASON.format(reason = reason)
    try:
        # attempt to start votekick
        votekick = Votekick.start(player, victim, reason)
        protocol.votekick = votekick
    except VotekickFailure as err:
        return str(err)

@name('cancel')
def cancel_votekick(connection):
    protocol = connection.protocol
    votekick = protocol.votekick
    if not votekick:
        return S_NO_VOTEKICK
    if connection in protocol.players:
        player = connection
        if (player is not votekick.instigator and not player.admin and
            not player.rights.cancel):
            return S_CANT_CANCEL
    
    votekick.end(S_RESULT_CANCELLED)

@name('n')
def vote_no(connection):
    protocol = connection.protocol
    player = connection
    
    votekick = protocol.votekick
    if not votekick:
        return S_NO_VOTEKICK
    return votekick.voteNo(player) or ""

@name('y')
def vote_yes(connection):
    protocol = connection.protocol
    player = connection
    
    votekick = protocol.votekick
    if not votekick:
        return S_NO_VOTEKICK
    return votekick.voteYes(player) or ""

@alias("tvk")
@admin
def togglevotekick(connection, *args):
    protocol = connection.protocol
    if len(args) == 0:
        protocol.votekick_enabled = not protocol.votekick_enabled
        return "Votekicking globally %s." % ['disabled', 'enabled'][protocol.votekick_enabled]
    try:
        player = get_player(protocol, '#' + args[0])
    except InvalidPlayer:
        player = get_player(protocol, args[0])
    player.votekick_enabled = not player.votekick_enabled
    return "Votekicking is %s for %s." % (['disabled', 'enabled'][player.votekick_enabled], player.name)

add(start_votekick)
add(cancel_votekick)
add(vote_no)
add(vote_yes)
add(togglevotekick)
class Votekick(object):
    duration = 30.0 # Time between messages
    repsBeforeKick = 3 #Times the message will com up before a kick nb- beginning a votekick does not count
    interval = 2 * 60.0 # 3 minutes
    ban_duration = 15.0
    public_votes = True
    schedule = None
    totalVotes = 0
    
    def _get_votes_remaining(self):
        return 100 #dummy for non fixed votekick things
    votes_remaining = property(_get_votes_remaining)
    
    @classmethod
    def start(cls, instigator, victim, reason = None):
        protocol = instigator.protocol
        last_votekick = instigator.last_votekick
        reason = reason.strip() if reason else None
        if protocol.votekick:
            raise VotekickFailure(S_IN_PROGRESS)
        elif instigator is victim:
            raise VotekickFailure(S_SELF_VOTEKICK)
        elif victim.admin or victim.rights.cancel:
            raise VotekickFailure(S_VOTEKICK_IMMUNE)
        elif not instigator.admin and (last_votekick is not None and
            seconds() - last_votekick < cls.interval):
            raise VotekickFailure(S_NOT_YET)
        elif REQUIRE_REASON and not reason:
            raise VotekickFailure(S_NEED_REASON)
        
        result = protocol.on_votekick_start(instigator, victim, reason)
        if result is not None:
            raise VotekickFailure(result)
        
        reason = reason or S_DEFAULT_REASON
        return cls(instigator, victim, reason)
    
    def __init__(self, instigator, victim, reason):
        self.protocol = protocol = instigator.protocol
        self.instigator = instigator
        self.victim = victim
        self.reason = reason
        self.votes = {instigator : True}
        self.ended = False
        
        protocol.irc_say(S_ANNOUNCE_IRC.format(instigator = instigator.name,
            victim = victim.name, reason = self.reason))
        protocol.send_chat(S_ANNOUNCE.format(instigator = instigator.name,
            victim = victim.name), sender = instigator)
        protocol.send_chat(S_REASON.format(reason = self.reason),
            sender = instigator)
        instigator.send_chat(S_ANNOUNCE_SELF.format(victim = victim.name))
        
        schedule = Scheduler(protocol)
        #schedule.call_later(self.duration, self.end,S_RESULT_TIMED_OUT)
        schedule.loop_call(self.duration, self.send_chat_update)
        self.schedule = schedule
    
    def voteYes(self, player):
        if self.victim is player:
            return S_VICTIM_VOTES
        elif player in self.votes:
            if self.votes[player]==True:
                return S_ALREADY_VOTED
        if self.public_votes:
            if player in self.votes:
                self.totalVotes+=1
                self.protocol.send_chat(S_CHANGED_YES.format(player = player.name)+". Now at (%d)"%(self.totalVotes+1))
            else:
                self.protocol.send_chat(S_YES.format(player = player.name)+". Now at (%d)"%(self.totalVotes+1))
        self.votes[player] = True
        self.totalVotes+=1

    def voteNo(self, player):
        if self.victim is player:
            return S_VICTIM_VOTES
        if self.instigator is player:
            self.end(S_RESULT_CANCELLED)
            return 
        elif player in self.votes:
            if self.votes[player]==False:
                return S_ALREADY_VOTED
        if self.public_votes:
            if player in self.votes:
                self.totalVotes-=1
                self.protocol.send_chat(S_CHANGED_NO.format(player = player.name)+". Now at (%d)"%(self.totalVotes-1))
            else:
                self.protocol.send_chat(S_NO.format(player = player.name)+". Now at (%d)"%(self.totalVotes-1))
        self.votes[player] = False
        self.totalVotes-=1

    def release(self):
        self.instigator = None
        self.victim = None
        self.votes = None
        if self.schedule:
            self.schedule.reset()
        self.schedule = None
        self.protocol.votekick = None
    
    def end(self, result):
        self.ended = True
        message = S_ENDED.format(victim = self.victim.name, result = result)
        name = self.victim.name
        admin=self.instigator.admin
        if result==S_RESULT_TIMED_OUT:
            if self.totalVotes>0:
                if self.ban_duration > 0.0:
                    self.victim.ban(self.reason, self.ban_duration)
                else:
                    self.victim.kick(silent = True)
                message = S_ENDED.format(victim = name, result = S_RESULT_TIMED_OUT+" "+S_RESULT_PASSED)
            else:
                message = S_ENDED.format(victim = name, result = S_RESULT_TIMED_OUT+" "+S_RESULT_FAILED)
        self.protocol.send_chat(message, irc = True)
        if not admin:
            self.instigator.last_votekick = seconds()
        self.protocol.on_votekick_end()
        self.release()
    
    def send_chat_update(self, target = None):
        # send only to target player if provided, otherwise broadcast to server
        target = target or self.protocol
        if self.repsBeforeKick<=0:
            self.end(S_RESULT_TIMED_OUT)
        else:
            target.send_chat(S_UPDATE.format(instigator = self.instigator.name,
                victim = self.victim.name, votes = self.totalVotes, duration = int(self.duration*self.repsBeforeKick), tnot = ["not ",""][self.totalVotes>0]))
            target.send_chat(S_REASON.format(reason = self.reason))
        self.repsBeforeKick-=1
        

def apply_script(protocol, connection, config):
    Votekick.ban_duration = config.get('votekick_ban_duration', 15.0)
    Votekick.public_votes = config.get('votekick_public_votes', True)
    required_percentage = config.get('votekick_percentage', 25.0)
    
    class VotekickProtocol(protocol):
        votekick = None
        
        def get_required_votes(self):
            # votekicks are invalid if this returns <= 0
            player_count = sum(not player.disconnected for player in
                self.players.itervalues()) - 1
            return int(player_count / 100.0 * required_percentage)+1
        
        def on_map_leave(self):
            if self.votekick:
                self.votekick.release()
            protocol.on_map_leave(self)
        
        def on_ban(self, banee, reason, duration):
            votekick = self.votekick
            if votekick and votekick.victim is self:
                votekick.end(S_RESULT_BANNED)
            protocol.on_ban(self, connection, reason, duration)
        
        def on_votekick_start(self, instigator, victim, reason):
            pass
        
        def on_votekick_end(self):
            pass
    
    class VotekickConnection(connection):
        last_votekick = None
        
        def on_disconnect(self):
            votekick = self.protocol.votekick
            if votekick:
                if votekick.victim is self:
                    # victim leaves, gets votekick ban
                    reason = votekick.reason
                    votekick.end(S_RESULT_LEFT.format(victim = self.name))
                    self.ban(reason, Votekick.ban_duration)
                elif votekick.instigator is self:
                    # instigator leaves, votekick is called off
                    s = S_RESULT_INSTIGATOR_LEFT.format(instigator = self.name)
                    votekick.end(s)
            connection.on_disconnect(self)
        
        def kick(self, reason = None, silent = False):
            votekick = self.protocol.votekick
            if votekick:
                if votekick.victim is self:
                    votekick.end(S_RESULT_KICKED)
                elif votekick.instigator is self:
                    votekick.end(S_RESULT_INSTIGATOR_KICKED)
            connection.kick(self, reason, silent)
    
    return VotekickProtocol, VotekickConnection
Attachments
votekick.py
(12.97 KiB) Downloaded 71 times
Last edited by thepolm3 on Fri Jul 12, 2013 5:09 pm, edited 7 times in total.
Buffet_of_Lies
Mapper
Mapper
Posts: 402
Joined: Tue Nov 20, 2012 11:25 am


I know some people are just going to hate this idea but I, for one, think it's not a bad concept. This always occurs to me when I watch votekicks going on.
topo
Global Moderator
Global Moderator
Posts: 179
Joined: Thu Nov 01, 2012 12:43 pm


It feels to me like the same reason many votekicks currently don't pass would cause many votekicks under this system to pass. Some people simply don't vote when they aren't certain and I don't think that should be treated as either a yes or a no. It's not a vote either way. If you *must* treat a non-vote as a vote, treating it as a "no" is better than treating it as a "yes".
thepolm3
Scripter
Scripter
Posts: 424
Joined: Sat Feb 16, 2013 10:49 pm


Yeah, I'll probably end up making a new version with both /n and /y.

EDIT
done!
Last edited by thepolm3 on Fri Mar 29, 2013 9:21 pm, edited 1 time in total.
TB_
Post Demon
Post Demon
Posts: 998
Joined: Tue Nov 20, 2012 6:59 pm


I wonder why /n got removed in the first place.
thepolm3
Scripter
Scripter
Posts: 424
Joined: Sat Feb 16, 2013 10:49 pm


Added a function to refuse votekicks that contain keywords
6 posts Page 1 of 1
Return to “Completed Releases”

Who is online

Users browsing this forum: No registered users and 1 guest