Page 1 of 2

Changing some of the default features in pyspades/pysnip

Posted: Sun Jan 06, 2013 8:42 am
by Sasquatch
It's really been bugging me for a while not that some people are using the plainly terrible default values when running their own server. Aimbots are slowly becoming a problem again and votekicking doesn't do any good considering 50% of the people who play games are often either oblivious on how to use votekicks or oblivious to typing in the chat entirely. I think that in the default config file for pyspades or pysnip or whatever we're using now should be changed to something like 20%.

On top of that, I think we should discuss here as a community what some of the other default features should be. Votekick percentages is just issue #1. I'll propose a couple things which I think will help solve problems the community has been facing for a while...

1. Don't add any maps initially to the cycle for map rotation in the configuration file. That way we can avoid having dozens of servers up at a time that are only playing bridge or anticbridge or whatever. Also, hosters will have to be more aware of the maps that are available and might make more conscious choices as to what's actually being played.

2. Extend time limit. Simple enough, it's a real pain when you're excited that you're team is just taking the lead and the score is 6 to 5 or something, and then you get a warning that there are like three minutes left. I remember around the time of v 2.1- 2.5 when maps would be kept up for days at a time and still stayed full. It wouldn't kill is to extend time limits by a few hours so that when someone joins a server with a map they like only to find out it's changing in a few moments.


I know these issues would be avoided if people took the time to read and go through each of the options that pyspades/pysnip offers, but let's be honest, most of them don't.

Re: Changing some of the default features in pyspades/pysnip

Posted: Sun Jan 06, 2013 9:59 pm
by GreaseMonkey
3. Drop the SMG headshot damage to 33. That way we won't have to flame people so much.

Re: Changing some of the default features in pyspades/pysnip

Posted: Mon Jan 07, 2013 8:44 am
by Sasquatch
GreaseMonkey wrote:
3. Drop the SMG headshot damage to 33. That way we won't have to flame people so much.
Umm, that's more of a programmed feature. I'm just talking about the default values in the config files

Re: Changing some of the default features in pyspades/pysnip

Posted: Mon Jan 07, 2013 9:12 am
by VladVP
This could be a good set of tips for people starting up new servers...

We might as well make this thread sticky... Green_BigSmile

Re: Changing some of the default features in pyspades/pysnip

Posted: Mon Jan 07, 2013 2:22 pm
by rakiru
As much as I'd love to just say "Well, if they don't bother setting up their server, it'll be shit and no-one will play on it, meaning it will die quickly.", I feel that you're right. Here's my suggestion for a default config:

For a start, I'd recommend moving the name away from the top of the file, in an attempt to make people read the rest of it.

Re: Changing some of the default features in pyspades/pysnip

Posted: Mon Jan 07, 2013 5:22 pm
by Sasquatch
rakiru wrote:
As much as I'd love to just say "Well, if they don't bother setting up their server, it'll be shit and no-one will play on it, meaning it will die quickly.", I feel that you're right. Here's my suggestion for a default config:

For a start, I'd recommend moving the name away from the top of the file, in an attempt to make people read the rest of it.
xD Might not be a bad idea. However, the map name and rules text shows up right after that. Noobs might see that and not think of it as much of a tutorial. While default values are used a lot of the time in these servers, I almost never see them named just pyspades server/pysnip server... anymore. Maybe the next release should come with a config help text file?

also w00t we agree on something! ;) sorry about my rock headed argument yesterday.

cough

Re: Changing some of the default features in pyspades/pysnip

Posted: Mon Jan 07, 2013 8:20 pm
by GreaseMonkey
Sasquatch wrote:
1. Don't add any maps initially to the cycle for map rotation in the configuration file. That way we can avoid having dozens of servers up at a time that are only playing bridge or anticbridge or whatever.
I want to punch whoever decided that anticbridge should be a default map. That map SUCKS.
rakiru wrote:
For a start, I'd recommend moving the name away from the top of the file, in an attempt to make people read the rest of it.
If you can require that people RTFM, that definitely helps. It means you can teach them important stuff, like "don't add anticbridge to the rotation".
Sasquatch wrote:
Maybe the next release should come with a config help text file?
If it's one of the files that aren't pure JSON (as in, they're actually Python), I guess you could slip in some comments. However, I have a sneaking suspicion this is the one place where they actually use a proper JSON parser.

Re: Changing some of the default features in pyspades/pysnip

Posted: Tue Jan 08, 2013 2:23 am
by rakiru
GreaseMonkey wrote:
If it's one of the files that aren't pure JSON (as in, they're actually Python), I guess you could slip in some comments. However, I have a sneaking suspicion this is the one place where they actually use a proper JSON parser.
The config is indeed actual JSON.

I say we set the default values for things like votekick to good ones, as that's something that's hard to judge without experience, but at the same time, remove all of the default maps (yes, all of them), and perhaps instead of clearing out the map list, change it to something like:
Code: Select all
"maps" : ["map1", "map2", "read-the-readme"]
so the server will simply not work if they haven't bothered learning how to set it up.

Re: Changing some of the default features in pyspades/pysnip

Posted: Tue Jan 08, 2013 7:17 am
by Sasquatch
rakiru wrote:
GreaseMonkey wrote:
If it's one of the files that aren't pure JSON (as in, they're actually Python), I guess you could slip in some comments. However, I have a sneaking suspicion this is the one place where they actually use a proper JSON parser.
The config is indeed actual JSON.

I say we set the default values for things like votekick to good ones, as that's something that's hard to judge without experience, but at the same time, remove all of the default maps (yes, all of them), and perhaps instead of clearing out the map list, change it to something like:
Code: Select all
"maps" : ["map1", "map2", "read-the-readme"]
so the server will simply not work if they haven't bothered learning how to set it up.
Image

Re: Changing some of the default features in pyspades/pysnip

Posted: Tue Jan 08, 2013 9:55 am
by Cajun Style
The admin password should have the same feature: don't run unless changed. dynfog and mapextensions should be standard too.

Re: Changing some of the default features in pyspades/pysnip

Posted: Tue Jan 08, 2013 11:09 am
by izzy
A default successful votekick of 20% is way too low and will get abused often on a public server. Even 50% gets abused. The current default of 25% is already low. How about increasing it to 35%?

The default map rotation has already been reduced to only 'random' and 'classicgen' in PySnip.

These are the defaults I'd like:

config.txt.default
Code: Select all
    {
        "name" : "A Default Server",
        "motd" : [
            "Welcome to %(server_name)s",
            "Map: %(map_name)s by %(map_author)s",
            "Game mode: Capture The Flag",
            "Server powered by PySnip and BuildAndShoot.com"
        ],
        "help" : [
            "Server name: %(server_name)s",
            "Map: %(map_name)s by %(map_author)s",
            "Game mode: Capture The Flag",
            "/STREAK    Shows how many kills in a row you got without dying",
            "/INTEL     Tells you who's got the enemy intel",
            "/VOTEKICK  Start a vote to temporarily ban a disruptive player",
            "/TIME      Remaining time until forced map reset"
        ],
        "tips" : [
            "You are playing Capture The Flag on %(server_name)s",
            "Type /help for info & commands"
        ],
        "tip_frequency" : 5,
        "rules" : [
            "Cheating isn't welcome. Griefing is frowned upon. Have fun!"
        ],
        "master" : true,
        "max_players" : 32,
        "max_connections_per_ip" : 3,
        "port" : 32887,
        "network_interface" : "",

        "game_mode" : "ctf",
        "cap_limit" : 10,
        "default_time_limit" : 120,
        "advance_on_win" : true,
        "maps" : ["classicgen", "random"],
        "random_rotation" : false,

        "respawn_time" : 16,
        "respawn_waves" : true,
        "friendly_fire" : "on_grief",
        "grief_friendly_fire_time" : 5,
        "spade_teamkills_on_grief" : false,
        "balanced_teams" : 2,
        "teamswitch_interval" : 0,

        "speedhack_detect" : false,
        "votekick_percentage" : 35,
        "votekick_ban_duration" : 30,
        "votekick_public_votes" : true,
        "votemap_public_votes" : true,
        "votemap_extension_time" : 15,
        "votemap_player_driven" : false,
        "votemap_autoschedule" : false,
        "votemap_time" : 120,
        "votemap_percentage" : 80,

        "melee_damage" : 80,
        "fall_damage" : true,
        "user_blocks_only" : false,
        "set_god_build" : false,
        "server_prefix" : "",
        "time_announcements" : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 30, 60, 120, 180,
                                240, 300, 600, 900, 1200, 1800, 2400, 3000],
        "login_retries" : 3,
        "default_ban_duration" : 1440,

        "logfile" : "./logs/log.txt",
        "rotate_daily" : true,
        "debug_log" : false,
        "profile" : false,

        "team1" : {
            "name" : "Blue",
            "color" : [0, 0, 255]
        },
        "team2" : {
            "name" : "Green",
            "color" : [0, 255, 0]
        },
       "passwords" : {
            "admin" : ["adminpass1", "adminpass2", "adminpass3"],
            "moderator" : ["modpass"],
            "guard" : ["guardpass"],
            "trusted" : ["trustedpass"]
        },
        "rights" : {
            "moderator" : ["advance", "cancel", "dban", "fog", "from", "goto", "hackinfo", "hban", "invisible", "ip", "kick", "kickafk", "kill", "map", "master", "move", "mute", "resetgame", "switch", "teleport", "teleport_other", "tpsilent", "togglebuild", "togglekill", "togglevotekick", "trust", "undoban", "unmute", "unstick", "where", "whowas"],
            "guard" : ["cancel", "fog", "from", "goto", "hackinfo", "hban", "ip", "kick", "kickafk", "kill", "move", "mute", "switch", "teleport", "teleport_other", "togglebuild", "togglekill", "togglevotekick", "trust", "unmute", "unstick", "where", "whowas"]
        },
        "ssh" : {
            "enabled" : false,
            "port" : 32887,
            "users" : {
                "user" : "ssh_pass_change_this"
            }
        },
        "status_server" : {
            "enabled" : false,
            "port" : 32886
        },
        "ban_publish" : {
            "enabled" : false,
            "port" : 32885
        },
        "ban_subscribe" : {
            "enabled" : true,
            "urls" : [
                ["http://www.blacklist.spadille.net/subscribe.json", []]
            ]
        },
        "irc" : {
            "enabled" : false,
            "nickname" : "PySnip",
            "username" : "PySnip",
            "realname" : "PySnip",
            "server" : "irc.quakenet.org",
            "port" : 6667,
            "channel" : "#MyServerChannel",
            "password" : "",
            "commandprefix" : "!",
            "chatprefix" : "."
        },
        "scripts" : [
            "rollback",
            "protect",
            "map_extensions",
            "disco",
            "votekick",
            "trusted",
            "ratio",
            "passreload",
            "blockinfo",
            "afk"
        ],

        "squad_respawn_time" : 32,
        "squad_size" : 4,
        "auto_squad" : false,
        "load_saved_map" : false,
        "rollback_on_game_end" : false,
        "afk_time_limit" : 30
    }
commands.py
Code: Select all
    # Copyright (c) Mathias Kaerlev 2011-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/>.

    import math
    from random import choice
    from pyspades.constants import *
    from pyspades.common import prettify_timespan
    from pyspades.server import parse_command
    from twisted.internet import reactor
    from map import check_rotation
    import inspect

    commands = {}
    aliases = {}
    rights = {}

    class InvalidPlayer(Exception):
        pass

    class InvalidSpectator(InvalidPlayer):
        pass

    class InvalidTeam(Exception):
        pass

    def add_rights(func_name, *user_types):
        for user_type in user_types:
            if user_type in rights:
                rights[user_type].add(func_name)
            else:
                rights[user_type] = set([func_name])

    def restrict(func, *user_types):
        def new_func(connection, *arg, **kw):
            return func(connection, *arg, **kw)
        new_func.func_name = func.func_name
        new_func.user_types = user_types
        new_func.argspec = inspect.getargspec(func)
        return new_func

    def has_rights(f, connection):
        return not hasattr(f, 'user_types') or f.func_name in connection.rights

    def admin(func):
        return restrict(func, 'admin')

    def name(name):
        def dec(func):
            func.func_name = name
            return func
        return dec

    def alias(name):
        def dec(func):
            try:
                func.aliases.append(name)
            except AttributeError:
                func.aliases = [name]
            return func
        return dec

    def get_player(protocol, value, spectators = True):
        ret = None
        try:
            if value.startswith('#'):
                value = int(value[1:])
                ret = protocol.players[value]
            else:
                players = protocol.players
                try:
                    ret = players[value]
                except KeyError:
                    value = value.lower()
                    for player in players.values():
                        name = player.name.lower()
                        if name == value:
                            return player
                        if name.count(value):
                            ret = player
        except (KeyError, IndexError, ValueError):
            pass
        if ret is None:
            raise InvalidPlayer()
        elif not spectators and ret.world_object is None:
            raise InvalidSpectator()
        return ret

    def get_team(connection, value):
        value = value.lower()
        if value == 'blue':
            return connection.protocol.blue_team
        elif value == 'green':
            return connection.protocol.green_team
        elif value == 'spectator':
            return connection.protocol.spectator_team
        raise InvalidTeam()

    def join_arguments(arg, default = None):
        if not arg:
            return default
        return ' '.join(arg)

    def parse_maps(pre_maps):
        maps = []
        for n in pre_maps:
            if n[0]=="#" and len(maps)>0:
                maps[-1] += " "+n
            else:
                maps.append(n)
       
        return maps, ', '.join(maps)

    @admin
    def kick(connection, value, *arg):
        reason = join_arguments(arg)
        player = get_player(connection.protocol, value)
        player.kick(reason)

    def get_ban_arguments(connection, arg):
        duration = None
        if len(arg):
            try:
                duration = int(arg[0])
                arg = arg[1:]
            except (IndexError, ValueError):
                pass
        if duration is None:
            if len(arg)>0 and arg[0] == "perma":
                arg = arg[1:]
            else:
                duration = connection.protocol.default_ban_time
        reason = join_arguments(arg)
        return duration, reason

    @admin
    def ban(connection, value, *arg):
        duration, reason = get_ban_arguments(connection, arg)
        player = get_player(connection.protocol, value)
        player.ban(reason, duration)

    @admin
    def hban(connection, value, *arg):
        duration = int(60)
        reason = join_arguments(arg)
        player = get_player(connection.protocol, value)
        player.ban(reason, duration)

    @admin
    def dban(connection, value, *arg):
        duration = int(1440)
        reason = join_arguments(arg)
        player = get_player(connection.protocol, value)
        player.ban(reason, duration)

    @admin
    def wban(connection, value, *arg):
        duration = int(10080)
        reason = join_arguments(arg)
        player = get_player(connection.protocol, value)
        player.ban(reason, duration)

    @admin
    def banip(connection, ip, *arg):
        duration, reason = get_ban_arguments(connection, arg)
        try:
            connection.protocol.add_ban(ip, reason, duration)
        except ValueError:
            return 'Invalid IP address/network'
        reason = ': ' + reason if reason is not None else ''
        duration = duration or None
        if duration is None:
            return 'IP/network %s permabanned%s' % (ip, reason)
        else:
            return 'IP/network %s banned for %s%s' % (ip,
                prettify_timespan(duration * 60), reason)

    @admin
    def unban(connection, ip):
        try:
            connection.protocol.remove_ban(ip)
            return 'IP unbanned'
        except KeyError:
            return 'IP not found in ban list'

    @name('undoban')
    @admin
    def undo_ban(connection, *arg):
        if len(connection.protocol.bans)>0:
            result = connection.protocol.undo_last_ban()
            return ('Ban for %s undone' % result[0])
        else:
            return 'No bans to undo!'

    @admin
    def say(connection, *arg):
        value = ' '.join(arg)
        connection.protocol.send_chat(value)
        connection.protocol.irc_say(value)

    add_rights('kill', 'admin')
    def kill(connection, value = None):
        if value is None:
            player = connection
        else:
            if not connection.rights.kill:
                return "You can't use this command"
            player = get_player(connection.protocol, value, False)
        player.kill()
        if connection is not player:
            message = '%s killed %s' % (connection.name, player.name)
            connection.protocol.send_chat(message, irc = True)

    @admin
    def heal(connection, player = None):
        if player is not None:
            player = get_player(connection.protocol, player, False)
            message = '%s was healed by %s' % (player.name, connection.name)
        else:
            if connection not in connection.protocol.players:
                raise ValueError()
            player = connection
            message = '%s was healed' % (connection.name)
        player.refill()
        connection.protocol.send_chat(message, irc = True)

    def rules(connection):
        if connection not in connection.protocol.players:
            raise KeyError()
        lines = connection.protocol.rules
        if lines is None:
            return
        connection.send_lines(lines)

    def help(connection):
        """
        This help
        """
        if connection.protocol.help is not None and not connection.admin:
            connection.send_lines(connection.protocol.help)
        else:
            names = [command.func_name for command in command_list
                if command.func_name in connection.rights]
            return 'Available commands: %s' % (', '.join(names))

    def login(connection, password):
        """
        Login as a user type
        """
        if connection not in connection.protocol.players:
            raise KeyError()
        for user_type, passwords in connection.protocol.passwords.iteritems():
            if password in passwords:
                if user_type in connection.user_types:
                    return "You're already logged in as %s" % user_type
                return connection.on_user_login(user_type, True)
        if connection.login_retries is None:
            connection.login_retries = connection.protocol.login_retries - 1
        else:
            connection.login_retries -= 1
        if not connection.login_retries:
            connection.kick('Ran out of login attempts')
            return
        return 'Invalid password - you have %s tries left' % (
            connection.login_retries)

    def pm(connection, value, *arg):
        player = get_player(connection.protocol, value)
        message = join_arguments(arg)
        player.send_chat('PM from %s: %s' % (connection.name, message))
        return 'PM sent to %s' % player.name

    @name('admin')
    def to_admin(connection, *arg):
        protocol = connection.protocol
        message = join_arguments(arg)
        if not message:
            return "Enter a message you want to send, like /admin I'm stuck"
        prefix = '(TO ADMINS)'
        irc_relay = protocol.irc_relay
        if irc_relay:
            if irc_relay.factory.bot and irc_relay.factory.bot.colors:
                prefix = '\x0304' + prefix + '\x0f'
            irc_relay.send(prefix + ' <%s> %s' % (connection.name, message))
        for player in protocol.players.values():
            if player.admin and player is not connection:
                player.send_chat('To ADMINS from %s: %s' %
                    (connection.name, message))
        return 'Message sent to admins'

    def streak(connection):
        if connection not in connection.protocol.players:
            raise KeyError()
        return ('Your current kill streak is %s. Best is %s kills' %
            (connection.streak, connection.best_streak))

    @admin
    def lock(connection, value):
        team = get_team(connection, value)
        team.locked = True
        connection.protocol.send_chat('%s team is now locked' % team.name)
        connection.protocol.irc_say('* %s locked %s team' % (connection.name,
            team.name))

    @admin
    def unlock(connection, value):
        team = get_team(connection, value)
        team.locked = False
        connection.protocol.send_chat('%s team is now unlocked' % team.name)
        connection.protocol.irc_say('* %s unlocked %s team' % (connection.name,
            team.name))

    @admin
    def switch(connection, player = None, team = None):
        protocol = connection.protocol
        if player is not None:
            player = get_player(protocol, player)
        elif connection in protocol.players:
            player = connection
        else:
            raise ValueError()
        if player.team.spectator:
            player.send_chat("The switch command can't be used on a spectating player.")
            return
        if team is None:
            new_team = player.team.other
        else:
            new_team = get_team(connection, team)
        if player.invisible:
            old_team = player.team
            player.team = new_team
            player.on_team_changed(old_team)
            player.spawn(player.world_object.position.get())
            player.send_chat('Switched to %s team' % player.team.name)
            if connection is not player and connection in protocol.players:
                connection.send_chat('Switched %s to %s team' % (player.name,
                    player.team.name))
            protocol.irc_say('* %s silently switched teams' % player.name)
        else:
            player.respawn_time = protocol.respawn_time
            player.set_team(new_team)
            protocol.send_chat('%s switched teams' % player.name, irc = True)

    @name('setbalance')
    @admin
    def set_balance(connection, value):
        try:
            value = int(value)
        except ValueError:
            return 'Invalid value %r. Use 0 for off, 1 and up for on' % value
        protocol = connection.protocol
        protocol.balanced_teams = value
        protocol.send_chat('Balanced teams set to %s' % value)
        connection.protocol.irc_say('* %s set balanced teams to %s' % (
            connection.name, value))

    @name('togglebuild')
    @alias('tb')
    @admin
    def toggle_build(connection, player = None):
        if player is not None:
            player = get_player(connection.protocol, player)
            value = not player.building
            player.building = value
            msg = '%s can build again' if value else '%s is disabled from building'
            connection.protocol.send_chat(msg % player.name)
            connection.protocol.irc_say('* %s %s building for %s' % (connection.name,
                ['disabled', 'enabled'][int(value)], player.name))
            return
        value = not connection.protocol.building
        connection.protocol.building = value
        on_off = ['OFF', 'ON'][int(value)]
        connection.protocol.send_chat('Building has been toggled %s!' % on_off)
        connection.protocol.irc_say('* %s toggled building %s' % (connection.name,
            on_off))
       
    @name('togglekill')
    @alias('tk')
    @admin
    def toggle_kill(connection, player = None):
        if player is not None:
            player = get_player(connection.protocol, player)
            value = not player.killing
            player.killing = value
            msg = '%s can kill again' if value else '%s is disabled from killing'
            connection.protocol.send_chat(msg % player.name)
            connection.protocol.irc_say('* %s %s killing for %s' % (connection.name,
                ['disabled', 'enabled'][int(value)], player.name))
            return
        value = not connection.protocol.killing
        connection.protocol.killing = value
        on_off = ['OFF', 'ON'][int(value)]
        connection.protocol.send_chat('Killing has been toggled %s!' % on_off)
        connection.protocol.irc_say('* %s toggled killing %s' % (connection.name,
            on_off))

    @name('toggleteamkill')
    @admin
    def toggle_teamkill(connection):
        value = not connection.protocol.friendly_fire
        connection.protocol.friendly_fire = value
        on_off = ['OFF', 'ON'][int(value)]
        connection.protocol.send_chat('Friendly fire has been toggled %s!' % on_off)
        connection.protocol.irc_say('* %s toggled friendly fire %s' % (
            connection.name, on_off))

    @admin
    def mute(connection, value):
        player = get_player(connection.protocol, value)
        if player.mute:
            return '%s is already muted' % player.name
        player.mute = True
        message = '%s has been muted by %s' % (player.name, connection.name)
        connection.protocol.send_chat(message, irc = True)

    @admin
    def unmute(connection, value):
        player = get_player(connection.protocol, value)
        if not player.mute:
            return '%s is not muted' % player.name
        player.mute = False
        message = '%s has been unmuted by %s' % (player.name, connection.name)
        connection.protocol.send_chat(message, irc = True)

    def deaf(connection, value = None):
        if value is not None:
            if not connection.admin and not connection.rights.deaf:
                return 'No administrator rights!'
            connection = get_player(connection.protocol, value)
        message = '%s deaf' % ('now' if not connection.deaf else 'no longer')
        connection.protocol.irc_say('%s is %s' % (connection.name, message))
        message = "You're " + message
        if connection.deaf:
            connection.deaf = False
            connection.send_chat(message)
        else:
            connection.send_chat(message)
            connection.deaf = True

    @name('globalchat')
    @admin
    def global_chat(connection):
        connection.protocol.global_chat = not connection.protocol.global_chat
        connection.protocol.send_chat('Global chat %s' % ('enabled' if
            connection.protocol.global_chat else 'disabled'), irc = True)

    @alias('tp')
    @admin
    def teleport(connection, player1, player2 = None, silent = False):
        player1 = get_player(connection.protocol, player1)
        if player2 is not None:
            if connection.admin or connection.rights.teleport_other:
                player, target = player1, get_player(connection.protocol, player2)
                silent = silent or player.invisible
                message = ('%s ' + ('silently ' if silent else '') + 'teleported '
                    '%s to %s')
                message = message % (connection.name, player.name, target.name)
            else:
                return 'No administrator rights!'
        else:
            if connection not in connection.protocol.players:
                raise ValueError()
            player, target = connection, player1
            silent = silent or player.invisible
            message = '%s ' + ('silently ' if silent else '') + 'teleported to %s'
            message = message % (player.name, target.name)
        player.set_location(target.get_location())
        if silent:
            connection.protocol.irc_say('* ' + message)
        else:
            connection.protocol.send_chat(message, irc = True)

    @admin
    def unstick(connection, player = None):
        if player is not None:
            player = get_player(connection.protocol, player)
        else:
            player = connection
        connection.protocol.send_chat("%s unstuck %s" %
            (connection.name, player.name), irc = True)
        player.set_location_safe(player.get_location())
         
    @alias('tps')
    @admin
    def tpsilent(connection, player1, player2 = None):
        teleport(connection, player1, player2, silent = True)

    from pyspades.common import coordinates, to_coordinates

    @name('goto')
    @admin
    def go_to(connection, value):
        if connection not in connection.protocol.players:
            raise KeyError()
        move(connection, connection.name, value, silent = connection.invisible)

    @admin
    def move(connection, player, value, silent = False):
        player = get_player(connection.protocol, player)
        x, y = coordinates(value)
        x += 32
        y += 32
        player.set_location((x, y, connection.protocol.map.get_height(x, y) - 2))
        if connection is player:
            message = ('%s ' + ('silently ' if silent else '') + 'teleported to '
                'location %s')
            message = message % (player.name, value.upper())
        else:
            message = ('%s ' + ('silently ' if silent else '') + 'teleported %s '
                'to location %s')
            message = message % (connection.name, player.name, value.upper())
        if silent:
            connection.protocol.irc_say('* ' + message)
        else:
            connection.protocol.send_chat(message, irc = True)   

    @admin
    def where(connection, value = None):
        if value is not None:
            connection = get_player(connection.protocol, value)
        elif connection not in connection.protocol.players:
            raise ValueError()
        x, y, z = connection.get_location()
        return '%s is in %s (%s, %s, %s)' % (connection.name,
            to_coordinates(x, y), int(x), int(y), int(z))

    @admin
    def god(connection, value = None):
        if value is not None:
            connection = get_player(connection.protocol, value)
        elif connection not in connection.protocol.players:
            raise ValueError()
        connection.god = not connection.god
        if connection.protocol.set_god_build:
            connection.god_build = connection.god
        else:
            connection.god_build = False
        if connection.god:
            message = '%s entered GOD MODE!' % connection.name
        else:
            message = '%s returned to being a mere human' % connection.name
        connection.protocol.send_chat(message, irc = True)

    @name('godbuild')
    @admin
    def god_build(connection, player = None):
        protocol = connection.protocol
        if player is not None:
            player = get_player(protocol, player)
        elif connection in protocol.players:
            player = connection
        else:
            raise ValueError()
        if not player.god:
            return 'Placing god blocks is only allowed in god mode'
        player.god_build = not player.god_build
       
        message = ('now placing god blocks' if player.god_build else
            'no longer placing god blocks')
        player.send_chat("You're %s" % message)
        if connection is not player and connection in protocol.players:
            connection.send_chat('%s is %s' % (player.name, message))
        protocol.irc_say('* %s is %s' % (player.name, message))

    @admin
    def fly(connection, player = None):
        protocol = connection.protocol
        if player is not None:
            player = get_player(protocol, player)
        elif connection in protocol.players:
            player = connection
        else:
            raise ValueError()
        player.fly = not player.fly
       
        message = 'now flying' if player.fly else 'no longer flying'
        player.send_chat("You're %s" % message)
        if connection is not player and connection in protocol.players:
            connection.send_chat('%s is %s' % (player.name, message))
        protocol.irc_say('* %s is %s' % (player.name, message))

    from pyspades.contained import KillAction
    from pyspades.server import create_player, set_tool, set_color, input_data, weapon_input
    from pyspades.common import make_color

    @alias('invis')
    @alias('inv')
    @admin
    def invisible(connection, player = None):
        protocol = connection.protocol
        if player is not None:
            player = get_player(protocol, player)
        elif connection in protocol.players:
            player = connection
        else:
            raise ValueError()
        player.invisible = not player.invisible
        player.filter_visibility_data = player.invisible
        player.god = player.invisible
        player.god_build = False
        player.killing = not player.invisible
        if player.invisible:
            player.send_chat("You're now invisible")
            protocol.irc_say('* %s became invisible' % player.name)
            kill_action = KillAction()
            kill_action.kill_type = choice([GRENADE_KILL, FALL_KILL])
            kill_action.player_id = kill_action.killer_id = player.player_id
            reactor.callLater(1.0 / NETWORK_FPS, protocol.send_contained,
                kill_action, sender = player)
        else:
            player.send_chat("You return to visibility")
            protocol.irc_say('* %s became visible' % player.name)
            x, y, z = player.world_object.position.get()
            create_player.player_id = player.player_id
            create_player.name = player.name
            create_player.x = x
            create_player.y = y
            create_player.z = z
            create_player.weapon = player.weapon
            create_player.team = player.team.id
            world_object = player.world_object
            input_data.player_id = player.player_id
            input_data.up = world_object.up
            input_data.down = world_object.down
            input_data.left = world_object.left
            input_data.right = world_object.right
            input_data.jump = world_object.jump
            input_data.crouch = world_object.crouch
            input_data.sneak = world_object.sneak
            input_data.sprint = world_object.sprint
            set_tool.player_id = player.player_id
            set_tool.value = player.tool
            set_color.player_id = player.player_id
            set_color.value = make_color(*player.color)
            weapon_input.primary = world_object.primary_fire
            weapon_input.secondary = world_object.secondary_fire
            protocol.send_contained(create_player, sender = player, save = True)
            protocol.send_contained(set_tool, sender = player)
            protocol.send_contained(set_color, sender = player, save = True)
            protocol.send_contained(input_data, sender = player)
            protocol.send_contained(weapon_input, sender = player)
        if connection is not player and connection in protocol.players:
            if player.invisible:
                return '%s is now invisible' % player.name
            else:
                return '%s is now visible' % player.name

    @admin
    def ip(connection, value = None):
        if value is None:
            if connection not in connection.protocol.players:
                raise ValueError()
            player = connection
        else:
            player = get_player(connection.protocol, value)
        return 'The IP of %s is %s' % (player.name, player.address[0])

    @name('whowas')
    @admin
    def who_was(connection, value):
        value = value.lower()
        ret = None
        exact_match = False
        for name, ip in connection.protocol.player_memory:
            name_lower = name.lower()
            if name_lower == value:
                ret = (name, ip)
                exact_match = True
            elif not exact_match and name_lower.count(value):
                ret = (name, ip)
        if ret is None:
            raise InvalidPlayer()
        return "%s's most recent IP was %s" % ret

    @name('resetgame')
    @admin
    def reset_game(connection):
        resetting_player = connection
        # irc compatibility
        if resetting_player not in connection.protocol.players:
            for player in connection.protocol.players.values():
                resetting_player = player
                if player.admin:
                    break
            if resetting_player is connection:
                return
        connection.protocol.reset_game(resetting_player)
        connection.protocol.on_game_end()
        connection.protocol.send_chat('Game has been reset by %s' % connection.name,
            irc = True)

    from map import Map
    import itertools

    @name('map')
    @admin
    def change_planned_map(connection, *pre_maps):
        name = connection.name
        protocol = connection.protocol

        # parse seed numbering
        maps, map_list = parse_maps(pre_maps)
        if not maps:
            return 'Invalid map name'
       
        map = maps[0]
        protocol.planned_map = check_rotation([map])[0]
        protocol.send_chat('%s changed next map to %s' % (name, map), irc = True)

    @name('rotation')
    @admin
    def change_rotation(connection, *pre_maps):
        name = connection.name
        protocol = connection.protocol

        maps, map_list = parse_maps(pre_maps)

        if len(maps) == 0:
            return 'Usage: /rotation <map1> <map2> <map3>...'
        ret = protocol.set_map_rotation(maps, False)
        if not ret:
            return 'Invalid map in map rotation (%s)' % ret.map
        protocol.send_chat("%s changed map rotation to %s." %
                                (name, map_list), irc=True)

    @name('rotationadd')
    @admin
    def rotation_add(connection, *pre_maps):
        name = connection.name
        protocol = connection.protocol

        new_maps, map_list = parse_maps(pre_maps)

        maps = connection.protocol.get_map_rotation()
        map_list = ", ".join(maps) + map_list
        maps.extend(new_maps)
       
        ret = protocol.set_map_rotation(maps, False)
        if not ret:
            return 'Invalid map in map rotation (%s)' % ret.map
        protocol.send_chat("%s added %s to map rotation." %
                                (name, " ".join(pre_maps)), irc=True)

    @name('showrotation')
    def show_rotation(connection):
        return ", ".join(connection.protocol.get_map_rotation())

    @name('revertrotation')
    @admin
    def revert_rotation(connection):
        protocol = connection.protocol
        maps = protocol.config['maps']
        protocol.set_map_rotation(maps, False)
        protocol.irc_say("* %s reverted map rotation to %s" % (name, maps))
       
    def mapname(connection):
        return 'Current map: ' + connection.protocol.map_info.name

    @admin
    def advance(connection):
        connection.protocol.advance_rotation('Map advance forced.')

    @name('timelimit')
    @admin
    def set_time_limit(connection, value):
        value = float(value)
        protocol = connection.protocol
        protocol.set_time_limit(value)
        protocol.send_chat('Time limit set to %s' % value, irc = True)

    @name('time')
    def get_time_limit(connection):
        advance_call = connection.protocol.advance_call
        if advance_call is None:
            return 'No time limit set'
        left = int(math.ceil((advance_call.getTime() - reactor.seconds()) / 60.0))
        return 'There are %s minutes left' % left

    @name('servername')
    @admin
    def server_name(connection, *arg):
        name = join_arguments(arg)
        protocol = connection.protocol
        protocol.config['name'] = name
        protocol.update_format()
        message = "%s changed servername to to '%s'" % (connection.name, name)
        print message
        connection.protocol.irc_say("* " + message)
        if connection in connection.protocol.players:
            return message

    @name('master')
    @admin
    def toggle_master(connection):
        protocol = connection.protocol
        protocol.set_master_state(not protocol.master)
        message = ("toggled master broadcast %s" % ['OFF', 'ON'][
            int(protocol.master)])
        protocol.irc_say("* %s " % connection.name + message)
        if connection in connection.protocol.players:
            return ("You " + message)

    def ping(connection, value = None):
        if value is None:
            if connection not in connection.protocol.players:
                raise ValueError()
            player = connection
        else:
            player = get_player(connection.protocol, value)
        ping = player.latency
        if value is None:
            return ('Your ping is %s ms. Lower ping is better!' % ping)
        return "%s's ping is %s ms" % (player.name, ping)

    def intel(connection):
        if connection not in connection.protocol.players:
            raise KeyError()
        flag = connection.team.other.flag
        if flag.player is not None:
            if flag.player is connection:
                return "You have the enemy intel, return to base!"
            else:
                return "%s has the enemy intel!" % flag.player.name
        return "Nobody in your team has the enemy intel"

    def version(connection):
        return 'Server version is "%s"' % connection.protocol.server_version

    @name('server')
    def server_info(connection):
        protocol = connection.protocol
        msg = 'You are playing on "%s"' % protocol.name
        if protocol.identifier is not None:
            msg += ' at %s' % protocol.identifier
        return msg

    def scripts(connection):
        scripts = connection.protocol.config.get('scripts', [])
        return 'Scripts enabled: %s' % (', '.join(scripts))

    @admin
    def fog(connection, r, g, b):
        r = int(r)
        g = int(g)
        b = int(b)
        connection.protocol.set_fog_color((r, g, b))

    def weapon(connection, value):
        player = get_player(connection.protocol, value)
        if player.weapon_object is None:
            name = '(unknown)'
        else:
            name = player.weapon_object.name
        return '%s has a %s' % (player.name, name)
       
    command_list = [
        help,
        pm,
        to_admin,
        login,
        kick,
        intel,
        ip,
        who_was,
        fog,
        ban,
        hban,
        dban,
        wban,
        banip,
        unban,
        undo_ban,
        mute,
        unmute,
        deaf,
        global_chat,
        say,
        kill,
        heal,
        lock,
        unlock,
        switch,
        set_balance,
        rules,
        toggle_build,
        toggle_kill,
        toggle_teamkill,
        teleport,
        tpsilent,
        go_to,
        move,
        unstick,
        where,
        god,
        god_build,
        fly,
        invisible,
        streak,
        reset_game,
        toggle_master,
        change_planned_map,
        change_rotation,
        revert_rotation,
        show_rotation,
        rotation_add,
        advance,
        set_time_limit,
        get_time_limit,
        server_name,
        ping,
        version,
        server_info,
        scripts,
        weapon,
        mapname
    ]

    def add(func, name = None):
        """
        Function to add a command from scripts
        """
        if name is None:
            name = func.func_name
        name = name.lower()
        if not hasattr(func, 'argspec'):
            func.argspec = inspect.getargspec(func)
        add_rights(name, *getattr(func, 'user_types', ()))
        commands[name] = func
        try:
            for alias in func.aliases:
                aliases[alias.lower()] = name
        except AttributeError:
            pass

    for command_func in command_list:
        add(command_func)

    # optional commands
    try:
        import pygeoip
        database = pygeoip.GeoIP('./data/GeoLiteCity.dat')
        @admin
        @name('from')
        def where_from(connection, value = None):
            if value is None:
                if connection not in connection.protocol.players:
                    raise ValueError()
                player = connection
            else:
                player = get_player(connection.protocol, value)
            record = database.record_by_addr(player.address[0])
            if record is None:
                return 'Player location could not be determined.'
            items = []
            for entry in ('country_name', 'city', 'region_name'):
                # sometimes, the record entries are numbers or nonexistent
                try:
                    value = record[entry]
                    int(value) # if this raises a ValueError, it's not a number
                    continue
                except KeyError:
                    continue
                except ValueError:
                    pass
                items.append(value)
            return '%s is from %s' % (player.name, ', '.join(items))
        add(where_from)
    except ImportError:
        print "('from' command disabled - missing pygeoip)"
    except (IOError, OSError):
        print "('from' command disabled - missing data/GeoLiteCity.dat)"

    def handle_command(connection, command, parameters):
        command = command.lower()
        try:
            command = aliases[command]
        except KeyError:
            pass
        try:
            command_func = commands[command]
        except KeyError:
            return # 'Invalid command'
        mn = len(command_func.argspec.args) - 1 - len(command_func.argspec.defaults or ())
        mx = len(command_func.argspec.args) - 1 if command_func.argspec.varargs is None else None
        lp = len(parameters)
        if lp < mn or mx is not None and lp > mx:
            return 'Invalid number of arguments for %s' % command
        try:
            if not has_rights(command_func, connection):
                return "You can't use this command"
            return command_func(connection, *parameters)
        except KeyError:
            return # 'Invalid command'
        except TypeError, t:
            print 'Command', command, 'failed with args:', parameters
            print t
            return 'Command failed'
        except InvalidPlayer:
            return 'No such player'
        except InvalidTeam:
            return 'Invalid team specifier'
        except ValueError:
            return 'Invalid parameters'

    def debug_handle_command(connection, command, parameters):
        # use this when regular handle_command eats errors
        if connection in connection.protocol.players:
            connection.send_chat("Commands are in DEBUG mode")
        command = command.lower()
        try:
            command = aliases[command]
        except KeyError:
            pass
        try:
            command_func = commands[command]
        except KeyError:
            return # 'Invalid command'
        if not has_rights(command_func, connection):
            return "You can't use this command"
        return command_func(connection, *parameters)

    # handle_command = debug_handle_command

    def handle_input(connection, input):
        # for IRC and console
        return handle_command(connection, *parse_command(input))
disco.py
Code: Select all
"""
Ever wanted a disco in Ace of Spades?

Maintainer: mat^2
"""

from twisted.internet.task import LoopingCall
from twisted.internet.reactor import callLater
import random

import commands

DISCO_ON_GAME_END = True
# Time is in seconds
DISCO_ON_GAME_END_DURATION = 10.0

@commands.name('disco')
@commands.admin
def toggle_disco(connection):
    connection.protocol.toggle_disco(True)

commands.add(toggle_disco)

DISCO_COLORS = set([
    (235, 64, 0),
    (128, 232, 121),
    (220, 223, 12),
    (43, 72, 228),
    (216, 94, 231),
    (255, 255, 255)
])

def apply_script(protocol, connection, config):
    class DiscoProtocol(protocol):
        current_colors = None
        disco = False
        old_fog_color = None
        def __init__(self, *arg, **kw):
            protocol.__init__(self, *arg, **kw)
            self.disco_loop = LoopingCall(self.update_color)
        
        def update_color(self):
            if not self.current_colors:
                self.current_colors = DISCO_COLORS.copy()
            color = self.current_colors.pop()
            self.set_fog_color(color)
        
        def on_game_end(self):
            if not self.disco and DISCO_ON_GAME_END:
                self.toggle_disco(False)
                callLater(DISCO_ON_GAME_END_DURATION, self.stop_disco)
            return protocol.on_game_end(self)
        
        def stop_disco(self):
            if self.disco:
                self.toggle_disco(False)

        def toggle_disco(self, message = False):
            self.disco = not self.disco
            if self.disco:
                self.old_fog_color = self.fog_color
                self.disco_loop.start(0.3)
                if message:
                    self.send_chat('DISCO PARTY MODE ENABLED!')
            else:
                self.disco_loop.stop()
                if self.old_fog_color is not None:
                    self.set_fog_color(self.old_fog_color)
                if message:
                    self.send_chat('The party has been stopped.')
    return DiscoProtocol, connection
votekick.py
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, get_player, join_arguments, InvalidPlayer, admin, alias

    REQUIRE_REASON = True

    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_YES = '{player} voted 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_PASSED = 'Player kicked'
    S_ANNOUNCE_IRC = '* {instigator} started a votekick against player {victim}. ' \
        'Reason: {reason}'
    S_ANNOUNCE = '{instigator} started a VOTEKICK against {victim}. Say /Y to agree'
    S_ANNOUNCE_SELF = 'You started a votekick against {victim}. Say /CANCEL to ' \
        'stop it'
    S_UPDATE = '{instigator} is votekicking {victim}. /Y to vote ({needed} left)'
    S_REASON = 'Reason: {reason}'

    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 protocol.votekick_enabled == False:
            return "Votekicking disabled"
        if player.votekick_enabled == False:
            return "You are not allowed to initiate a votekick."
       
        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:])
       
        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('y')
    def vote_yes(connection):
        protocol = connection.protocol
        if connection not in protocol.players:
            raise KeyError()
        player = connection
       
        votekick = protocol.votekick
        if not votekick:
            return S_NO_VOTEKICK
       
        votekick.vote(player)

    @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_yes)
    add(togglevotekick)

    class Votekick(object):
        duration = 120.0 # 2 minutes
        interval = 2 * 60.0 # 3 minutes
        ban_duration = 15.0
        public_votes = True
        schedule = None
       
        def _get_votes_remaining(self):
            return self.protocol.get_required_votes() - len(self.votes) + 1
        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 protocol.get_required_votes() <= 0:
                raise VotekickFailure(S_NOT_ENOUGH_PLAYERS)
            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(30.0, self.send_chat_update)
            self.schedule = schedule
       
        def vote(self, player):
            if self.victim is player:
                return
            elif player in self.votes:
                return
            if self.public_votes:
                self.protocol.send_chat(S_YES.format(player = player.name))
            self.votes[player] = True
            if self.votes_remaining <= 0:
                # vote passed, ban or kick accordingly
                victim = self.victim
                self.end(S_RESULT_PASSED)
                print '%s votekicked' % victim.name
                if self.ban_duration > 0.0:
                    victim.ban(self.reason, self.ban_duration)
                else:
                    victim.kick(silent = True)
       
        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)
            self.protocol.send_chat(message, irc = True)
            if not self.instigator.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
            target.send_chat(S_UPDATE.format(instigator = self.instigator.name,
                victim = self.victim.name, needed = self.votes_remaining))
            target.send_chat(S_REASON.format(reason = self.reason))

    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
            votekick_enabled = True
           
            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)
           
            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
            votekick_enabled = True
           
            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)
                    else:
                        # make sure we still have enough players
                        votekick.votes.pop(self, None)
                        if votekick.votes_remaining <= 0:
                            votekick.end(S_NOT_ENOUGH_PLAYERS)
                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
passreload.py
Code: Select all
    # passreload.py
    # written by Danke

    import commands
    from commands import add, admin
    import json

    @admin
    def reloadconfig(connection):
        new_config = {}
        try:
            new_config = json.load(open('config.txt', 'r'))
            if not isinstance(new_config, dict):
                raise ValueError('config.txt is not a mapping type')
        except ValueError, v:
            print 'Error reloading config:', v
            return 'Error reloading config. Check pyspades log for details.'
        connection.protocol.config.update(new_config)
        connection.protocol.reload_passes()
        return 'Config reloaded!'

    add(reloadconfig)

    def apply_script(protocol, connection, config):
        class PassreloadProtocol(protocol):
            def reload_passes(self):
                self.passwords = config.get('passwords', {})
                for password in self.passwords.get('admin', []):
                    if password == 'replaceme':
                        print 'REMEMBER TO CHANGE THE DEFAULT ADMINISTRATOR PASSWORD!'
                    elif not password:
                        self.everyone_is_admin = True
                commands.rights.update(config.get('rights', {}))
        return PassreloadProtocol, connection

Re: Changing some of the default features in pyspades/pysnip

Posted: Tue Jan 08, 2013 5:15 pm
by Sasquatch
izzy wrote:
A default successful votekick of 20% is way too low and will get abused often on a public server. Even 50% gets abused. The current default of 25% is already low. How about increasing it to 35%?
Nooooo are you out of your mind? Also did you read the rest of the thread? This doesn't have to be entirely about scripts.
Cajun Style wrote:
The admin password should have the same feature: don't run unless changed. dynfog and mapextensions should be standard too.
I think map_extensions is. But dynfog would be a smart choice since the pyspades fog is so ugly.

Re: Changing some of the default features in pyspades/pysnip

Posted: Tue Jan 08, 2013 8:47 pm
by rakiru
Cajun Style wrote:
The admin password should have the same feature: don't run unless changed. dynfog and mapextensions should be standard too.
No no no no no... If they can't even bother setting up their server, then why should they be protected from their own stupidity and laziness?
izzy wrote:
A default successful votekick of 20% is way too low and will get abused often on a public server. Even 50% gets abused. The current default of 25% is already low. How about increasing it to 35%?
See the votekick thread for discussion on this.

Re: Changing some of the default features in pyspades/pysnip

Posted: Tue Jan 08, 2013 9:29 pm
by izzy
Sasquatch wrote:
izzy wrote:
A default successful votekick of 20% is way too low and will get abused often on a public server. Even 50% gets abused. The current default of 25% is already low. How about increasing it to 35%?
Nooooo are you out of your mind? Also did you read the rest of the thread? This doesn't have to be entirely about scripts.
I'm saying this from my personal experience of monitoring many AoS servers for a long time; a low % will get abused and legit players will get kicked waaay more often than "bad" players will get removed. Here's just one example that happens all the time at 50%: http://buildandshoot.com/viewtopic.php?f=18&t=821#p7567.

My suggested config.txt.default includes additional or modified scripts in the script list, that's why I included the actual scripts as well. passreload.py allows admins to refresh config passwords without restarting the server, for example.

Re: Changing some of the default features in pyspades/pysnip

Posted: Wed Jan 09, 2013 1:28 am
by Ki11aWi11
Would you guys mind if I added some of my maps to the original pyspades map repository, just a select few such as trenches, none of the Arena maps or infiltration maps.