├── .gitignore ├── README.md ├── aimblock.py ├── aimbot2.py ├── antispawnkill.py ├── arena.py ├── avx.py ├── babel.py ├── badmin.py ├── box.py ├── buildbox.py ├── cbc.py ├── clearbox.py ├── db.py ├── df.py ├── dirtnade.py ├── dw.py ├── dynfog.py ├── floor.py ├── freeforall.py ├── gradient.py ├── hacktools.py ├── hp.py ├── jail.py ├── kraken.py ├── mapmakingtools.py ├── melee.py ├── meleerotation.py ├── onectf.py ├── quickbuild.py ├── rampage.py ├── rapid.py ├── ratio.py ├── reloadconfig.py ├── removesquad.py ├── rollback.py ├── savemap.py ├── smartnade.py ├── spectatorcontrol.py ├── streak.py ├── teamchat.py ├── teamchat_mute.py ├── timedmute.py ├── votekick.py ├── wall.py └── zombies.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pyspades-userscripts 2 | ==================== 3 | 4 | pyspades scripts, made and maintained by the community. 5 | 6 | Getting commit rights 7 | ===================== 8 | 9 | Please contact "mat^2" on #pyspades @ QuakeNet for commit rights. 10 | The repository is meant to be very open and autonomous. Pull requests will 11 | probably not be reviewed unless someone wants to volunteer. -------------------------------------------------------------------------------- /aimblock.py: -------------------------------------------------------------------------------- 1 | # AImBlock - aimbot detection script 2 | # 3 | # The point of this script is to detect aimbots 4 | # WITHOUT resorting to a hit-to-miss ratio. 5 | # 6 | # Current detection methods: 7 | # - if one changes target, determine how accurate the gun is with respect to the head 8 | # (currently disabled by default, enable below if you want it - the heuristic SUCKS) 9 | # - if one gets a lot of kills quickly it will warn the admins over IRC 10 | # 11 | # There are more possible methods that can be used, but for now, this should work. 12 | # I'm a bit worried about false positives though. 13 | # 14 | # - Ben "GreaseMonkey" Russell 15 | 16 | from twisted.internet import reactor 17 | import commands 18 | 19 | # disable if you don't want to see all the jerk information 20 | AIMBLOCK_SPAM = False 21 | 22 | # disable if you don't want to kick people who jerk conviniently onto their targets 23 | # note, needs more tweaking, also might not catch hooch's aimbot 24 | # ultimately needs lag compensation to be effective, which it doesn't have 25 | AIMBLOCK_KICK_JERK = False 26 | 27 | # disable if you don't want to kick people who jerk conviniently backwards onto their targets 28 | # -- NOT IMPLEMENTED! 29 | AIMBLOCK_KICK_SNAP = False 30 | 31 | def aimbotcheck(connection, user, minutes): 32 | connection = commands.get_player(connection.protocol, user) 33 | if connection not in connection.protocol.players: 34 | raise KeyError() 35 | kills = connection.tally_kill_log(reactor.seconds() - int(minutes)*60) 36 | return ('Player %s did %s kills in the last %s minutes.' % 37 | (connection.name, kills, minutes)) 38 | commands.add(aimbotcheck) 39 | 40 | def apply_script(protocol, connection, config): 41 | def aimblock(f): 42 | def _f1(self, *args, **kwargs): 43 | if self.aimbot_detect: 44 | return f(self, *args, **kwargs) 45 | return _f1 46 | 47 | class AImBlockConnection(connection): 48 | aimbot_detect = True 49 | aimbot_heur_max = 0.92 50 | aimbot_heur_jerk = 0.33 51 | aimbot_heur_leeway = 0.9 52 | aimbot_heur_snap_thres = -0.1 53 | aimbot_heur_snap_score = 1.2 54 | 55 | aimbot_heuristic = 0.0 56 | aimbot_target = None 57 | aimbot_orient_uv = (1.0, 0.0, 0.0) 58 | 59 | aimbot_kill_time = 30.0 60 | aimbot_kill_count = 10.0 61 | aimbot_kill_log = None 62 | aimbot_kill_warn_last = -3000.0 63 | aimbot_kill_warn_pause = 30.0 64 | 65 | def aimbot_record_kill(self): 66 | curkill = reactor.seconds() 67 | 68 | if self.aimbot_kill_log == None: 69 | self.aimbot_kill_log = [] 70 | 71 | self.aimbot_kill_log.append(curkill) 72 | 73 | if self.tally_kill_log(self.aimbot_kill_time) >= self.aimbot_kill_count: 74 | self.aimbot_trywarn() 75 | 76 | def tally_kill_log(self, seconds): 77 | if self.aimbot_kill_log == None: 78 | return 0 79 | 80 | i = -1 81 | while i >= -len(self.aimbot_kill_log): 82 | t = self.aimbot_kill_log[i] 83 | if t < seconds: 84 | break 85 | i -= 1 86 | 87 | return -1-i 88 | 89 | def aimbot_trywarn(self): 90 | curtime = reactor.seconds() 91 | if curtime < self.aimbot_kill_warn_last+self.aimbot_kill_warn_pause: 92 | return 93 | self.aimbot_kill_warn_last = curtime 94 | aimwarn = "AIMBOT WARNING: Player \"%s\" got %d kills in the last %d seconds!" % ( 95 | self.name, self.tally_kill_log(self.aimbot_kill_time), self.aimbot_kill_time 96 | ) 97 | self.protocol.irc_say(aimwarn) 98 | 99 | def on_kill(self, killer, type, grenade): 100 | if killer != None and killer != self: 101 | killer.aimbot_record_kill() 102 | return connection.on_kill(self, killer, type, grenade) 103 | 104 | def loader_received(self, loader): 105 | ret = connection.loader_received(self, loader) 106 | 107 | if not self.aimbot_detect: 108 | return ret 109 | 110 | chtarg = False 111 | 112 | if self.hp: 113 | if self.player_id is not None: 114 | chtarg = True 115 | self.recalc_orient_uv() 116 | 117 | if chtarg: 118 | self.get_aimbot_target() 119 | 120 | return ret 121 | 122 | def sub_vec(self, (x1, y1, z1), (x2, y2, z2)): 123 | return ((x1-x2),(y1-y2),(z1-z2)) 124 | 125 | def calc_uv(self, (x, y, z)): 126 | d = (x*x + y*y + z*z)**0.5 127 | if d <= 0.001: 128 | d = 0.001 129 | 130 | x /= d 131 | y /= d 132 | z /= d 133 | 134 | return (x,y,z) 135 | 136 | def recalc_orient_uv(self): 137 | ox, oy, oz = self.world_object.orientation.get() 138 | 139 | self.aimbot_orient_uv = self.calc_uv((ox, oy, oz)) 140 | 141 | def dot_product(self, v1, v2): 142 | x1, y1, z1 = v1 143 | x2, y2, z2 = v2 144 | 145 | return x1*x2 + y1*y2 + z1*z2 146 | 147 | @aimblock 148 | def get_aimbot_target(self): 149 | oldtarget = self.aimbot_target 150 | 151 | # find best target 152 | ftarg = None 153 | fdist = 0.0 154 | locpos = self.world_object.position.get() 155 | 156 | for pid in xrange(32): 157 | if pid not in self.protocol.players: 158 | continue 159 | p = self.protocol.players[pid] 160 | 161 | if pid == self.player_id: 162 | continue 163 | if p.team == self.team: 164 | continue 165 | 166 | xpos = p.world_object.position.get() 167 | xdist = self.dot_product( 168 | self.calc_uv(self.sub_vec(xpos,locpos)), 169 | self.aimbot_orient_uv 170 | ) 171 | if xdist > fdist: 172 | ftarg = p 173 | fdist = xdist 174 | 175 | # if we haven't found one, return 176 | if ftarg == None: 177 | return 178 | 179 | # do a quick unit vector check 180 | # TODO: proper triangulation check 181 | 182 | odist = 0.0 183 | if oldtarget != None: 184 | opos = oldtarget.world_object.position.get() 185 | odist = self.dot_product( 186 | self.calc_uv(self.sub_vec(opos,locpos)), 187 | self.aimbot_orient_uv 188 | ) 189 | 190 | if (oldtarget != None and oldtarget != ftarg 191 | and odist < self.aimbot_heur_leeway): 192 | self.aimbot_heuristic += ( 193 | (fdist-self.aimbot_heuristic) 194 | * self.aimbot_heur_jerk 195 | ) 196 | if AIMBLOCK_SPAM: 197 | print "Jerk test: %.5f -> %.5f" % ( 198 | fdist, self.aimbot_heuristic) 199 | 200 | self.aimbot_target = ftarg 201 | 202 | self.aimblock_try_complain() 203 | 204 | @aimblock 205 | def aimblock_try_complain(self): 206 | if self.aimbot_heuristic >= self.aimbot_heur_max: 207 | if AIMBLOCK_KICK_JERK: 208 | self.on_hack_attempt('Aimbot detected') 209 | else: 210 | self.aimbot_trywarn() 211 | 212 | return protocol, AImBlockConnection 213 | -------------------------------------------------------------------------------- /aimbot2.py: -------------------------------------------------------------------------------- 1 | from twisted.internet.task import LoopingCall 2 | from pyspades.constants import * 3 | from math import sqrt, cos, acos, pi, tan 4 | from commands import add, admin, get_player 5 | from twisted.internet import reactor 6 | import re 7 | 8 | DISABLED, KICK, BAN, WARN_ADMIN = xrange(4) 9 | 10 | # This is an option for data collection. Data is outputted to aimbot2log.txt 11 | DATA_COLLECTION = False 12 | 13 | # This controls which detection methods are enabled. If a player is detected 14 | # using one of these methods, the player is kicked. 15 | HEADSHOT_SNAP = WARN_ADMIN 16 | HIT_PERCENT = WARN_ADMIN 17 | KILLS_IN_TIME = WARN_ADMIN 18 | MULTIPLE_BULLETS = WARN_ADMIN 19 | 20 | DETECT_DAMAGE_HACK = True 21 | 22 | # Minimum amount of time that must pass between admin warnings that are 23 | # triggered by the same detection method. Time is in seconds. 24 | WARN_INTERVAL_MINIMUM = 300 25 | 26 | # These controls are only used if banning is enabled 27 | # Time is given in minutes. Set to 0 for a permaban 28 | HEADSHOT_SNAP_BAN_DURATION = 1400 29 | HIT_PERCENT_BAN_DURATION = 1440 30 | KILLS_IN_TIME_BAN_DURATION = 2880 31 | MULTIPLE_BULLETS_BAN_DURATION = 10080 32 | 33 | # If more than or equal to this number of weapon hit packets are recieved 34 | # from the client in half the weapon delay time, then an aimbot is detected. 35 | # This method of detection should have 100% detection and no false positives 36 | # with the current aimbot. 37 | # Note that the current aimbot does not modify the number of bullets 38 | # of the shotgun, so this method will not work if the player uses a shotgun. 39 | # These values may need to be changed if an update to the aimbot is released. 40 | RIFLE_MULTIPLE_BULLETS_MAX = 8 41 | SMG_MULTIPLE_BULLETS_MAX = 8 42 | 43 | # The minimum number of near misses + hits that are fired before kicking, 44 | # banning, or warning an admin about someone using the hit percentage check 45 | RIFLE_KICK_MINIMUM = 45 46 | SMG_KICK_MINIMUM = 90 47 | SHOTGUN_KICK_MINIMUM = 45 48 | 49 | # Kick, ban, or warn when the above minimum is met and the 50 | # bullet hit percentage is greater than or equal to this amount 51 | RIFLE_KICK_PERC = 0.90 52 | SMG_KICK_PERC = 0.80 53 | SHOTGUN_KICK_PERC = 0.90 54 | 55 | # If a player gets more kills than the KILL_THRESHOLD in the given 56 | # KILL_TIME, kick, ban, or warn. This check is performed every 57 | # time somebody kills someone with a gun 58 | KILL_TIME = 20.0 59 | KILL_THRESHOLD = 15 60 | 61 | # If the number of headshot snaps exceeds the HEADSHOT_SNAP_THRESHOLD in the 62 | # given HEADSHOT_SNAP_TIME, kick, ban, or warn. This check is performed every 63 | # time somebody performs a headshot snap 64 | HEADSHOT_SNAP_TIME = 20.0 65 | HEADSHOT_SNAP_THRESHOLD = 6 66 | 67 | # When the user's orientation angle (degrees) changes more than this amount, 68 | # check if the user snapped to an enemy's head. If it is aligned with a head, 69 | # record this as a headshot snap 70 | HEADSHOT_SNAP_ANGLE = 90.0 71 | 72 | # A near miss occurs when the player is NEAR_MISS_ANGLE degrees or less off 73 | # of an enemy 74 | NEAR_MISS_ANGLE = 10.0 75 | 76 | # Valid damage values for each gun 77 | RIFLE_DAMAGE = (33, 49, 100) 78 | SMG_DAMAGE = (18, 29, 75) 79 | SHOTGUN_DAMAGE = (16, 27, 37) 80 | 81 | # Approximate size of player's heads in blocks 82 | HEAD_RADIUS = 0.7 83 | 84 | # 128 is the approximate fog distance, but bump it up a little 85 | # just in case 86 | FOG_DISTANCE = 135.0 87 | 88 | # Don't touch any of this stuff 89 | FOG_DISTANCE2 = FOG_DISTANCE**2 90 | NEAR_MISS_COS = cos(NEAR_MISS_ANGLE * (pi/180.0)) 91 | HEADSHOT_SNAP_ANGLE_COS = cos(HEADSHOT_SNAP_ANGLE * (pi/180.0)) 92 | 93 | aimbot_pattern = re.compile(".*(aim|bot|ha(ck|x)|cheat).*", re.IGNORECASE) 94 | 95 | def aimbot_match(msg): 96 | return (not aimbot_pattern.match(msg) is None) 97 | 98 | def point_distance2(c1, c2): 99 | if c1.world_object is not None and c2.world_object is not None: 100 | p1 = c1.world_object.position 101 | p2 = c2.world_object.position 102 | return (p1.x - p2.x)**2 + (p1.y - p2.y)**2 + (p1.z - p2.z)**2 103 | 104 | def dot3d(v1, v2): 105 | return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2] 106 | 107 | def magnitude(v): 108 | return sqrt(v[0]**2 + v[1]**2 + v[2]**2) 109 | 110 | def scale(v, scale): 111 | return (v[0]*scale, v[1]*scale, v[2]*scale) 112 | 113 | def subtract(v1, v2): 114 | return (v1[0]-v2[0], v1[1]-v2[1], v1[2]-v2[2]) 115 | 116 | def accuracy(connection, name = None): 117 | if name is None: 118 | player = connection 119 | else: 120 | player = get_player(connection.protocol, name) 121 | return accuracy_player(player) 122 | 123 | def accuracy_player(player, name_info = True): 124 | if player.rifle_count != 0: 125 | rifle_percent = str(int(100.0 * (float(player.rifle_hits)/float(player.rifle_count)))) + '%' 126 | else: 127 | rifle_percent = 'None' 128 | if player.smg_count != 0: 129 | smg_percent = str(int(100.0 * (float(player.smg_hits)/float(player.smg_count)))) + '%' 130 | else: 131 | smg_percent = 'None' 132 | if player.shotgun_count != 0: 133 | shotgun_percent = str(int(100.0 * (float(player.shotgun_hits)/float(player.shotgun_count)))) + '%' 134 | else: 135 | shotgun_percent = 'None' 136 | s = '' 137 | if name_info: 138 | s += player.name + ' has an accuracy of: ' 139 | s += 'Rifle: %s SMG: %s Shotgun: %s.' % (rifle_percent, smg_percent, shotgun_percent) 140 | return s 141 | 142 | add(accuracy) 143 | 144 | @admin 145 | def hackinfo(connection, name): 146 | player = get_player(connection.protocol, name) 147 | return hackinfo_player(player) 148 | 149 | def hackinfo_player(player): 150 | info = "%s #%s (%s) has an accuracy of: " % (player.name, player.player_id, player.address[0]) 151 | info += accuracy_player(player, False) 152 | ratio = player.ratio_kills/float(max(1,player.ratio_deaths)) 153 | info += " Kill-death ratio of %.2f (%s kills, %s deaths)." % (ratio, player.ratio_kills, player.ratio_deaths) 154 | info += " %i kills in the last %i seconds." % (player.get_kill_count(), KILL_TIME) 155 | info += " %i headshot snaps in the last %i seconds." % (player.get_headshot_snap_count(), HEADSHOT_SNAP_TIME) 156 | return info 157 | 158 | add(hackinfo) 159 | 160 | def apply_script(protocol, connection, config): 161 | class Aimbot2Protocol(protocol): 162 | def start_votekick(self, payload): 163 | if aimbot_match(payload.reason): 164 | payload.target.warn_admin('Hack related votekick.') 165 | return protocol.start_votekick(self, payload) 166 | 167 | class Aimbot2Connection(connection): 168 | def __init__(self, *arg, **kw): 169 | connection.__init__(self, *arg, **kw) 170 | self.rifle_hits = self.smg_hits = self.shotgun_hits = 0 171 | self.rifle_count = self.smg_count = self.shotgun_count = 0 172 | self.last_target = None 173 | self.first_orientation = True 174 | self.kill_times = [] 175 | self.headshot_snap_times = [] 176 | self.bullet_loop = LoopingCall(self.on_bullet_fire) 177 | self.shot_time = 0.0 178 | self.multiple_bullets_count = 0 179 | self.headshot_snap_warn_time = self.hit_percent_warn_time = 0.0 180 | self.kills_in_time_warn_time = self.multiple_bullets_warn_time = 0.0 181 | 182 | def warn_admin(self, prefix = 'Possible aimbot detected.'): 183 | prefix += ' ' 184 | message = hackinfo_player(self) 185 | for player in self.protocol.players.values(): 186 | if player.admin: 187 | player.send_chat(prefix + message) 188 | irc_relay = self.protocol.irc_relay 189 | if irc_relay: 190 | if irc_relay.factory.bot and irc_relay.factory.bot.colors: 191 | prefix = '\x0304' + prefix + '\x0f' 192 | irc_relay.send(prefix + message) 193 | 194 | def on_spawn(self, pos): 195 | self.first_orientation = True 196 | return connection.on_spawn(self, pos) 197 | 198 | def bullet_loop_start(self, interval): 199 | if not self.bullet_loop.running: 200 | self.bullet_loop.start(interval) 201 | 202 | def bullet_loop_stop(self): 203 | if self.bullet_loop.running: 204 | self.bullet_loop.stop() 205 | 206 | def get_headshot_snap_count(self): 207 | pop_count = 0 208 | headshot_snap_count = 0 209 | current_time = reactor.seconds() 210 | for old_time in self.headshot_snap_times: 211 | if current_time - old_time <= HEADSHOT_SNAP_TIME: 212 | headshot_snap_count += 1 213 | else: 214 | pop_count += 1 215 | for i in xrange(0, pop_count): 216 | self.headshot_snap_times.pop(0) 217 | return headshot_snap_count 218 | 219 | def on_orientation_update(self, x, y, z): 220 | if not self.first_orientation and self.world_object is not None: 221 | orient = self.world_object.orientation 222 | old_orient_v = (orient.x, orient.y, orient.z) 223 | new_orient_v = (x, y, z) 224 | theta = dot3d(old_orient_v, new_orient_v) 225 | if theta <= HEADSHOT_SNAP_ANGLE_COS: 226 | self_pos = self.world_object.position 227 | for enemy in self.team.other.get_players(): 228 | enemy_pos = enemy.world_object.position 229 | position_v = (enemy_pos.x - self_pos.x, enemy_pos.y - self_pos.y, enemy_pos.z - self_pos.z) 230 | c = scale(new_orient_v, dot3d(new_orient_v, position_v)) 231 | h = magnitude(subtract(position_v, c)) 232 | if h <= HEAD_RADIUS: 233 | current_time = reactor.seconds() 234 | self.headshot_snap_times.append(current_time) 235 | if self.get_headshot_snap_count() >= HEADSHOT_SNAP_THRESHOLD: 236 | if HEADSHOT_SNAP == BAN: 237 | self.ban('Aimbot detected - headshot snap', HEADSHOT_SNAP_BAN_DURATION) 238 | return 239 | elif HEADSHOT_SNAP == KICK: 240 | self.kick('Aimbot detected - headshot snap') 241 | return 242 | elif HEADSHOT_SNAP == WARN_ADMIN: 243 | if (current_time - self.headshot_snap_warn_time) > WARN_INTERVAL_MINIMUM: 244 | self.headshot_snap_warn_time = current_time 245 | self.warn_admin() 246 | else: 247 | self.first_orientation = False 248 | return connection.on_orientation_update(self, x, y, z) 249 | 250 | def on_shoot_set(self, shoot): 251 | if self.tool == WEAPON_TOOL: 252 | if shoot and not self.bullet_loop.running: 253 | self.possible_targets = [] 254 | for enemy in self.team.other.get_players(): 255 | if point_distance2(self, enemy) <= FOG_DISTANCE2: 256 | self.possible_targets.append(enemy) 257 | self.bullet_loop_start(self.weapon_object.delay) 258 | elif not shoot: 259 | self.bullet_loop_stop() 260 | return connection.on_shoot_set(self, shoot) 261 | 262 | def get_kill_count(self): 263 | current_time = reactor.seconds() 264 | kill_count = 0 265 | pop_count = 0 266 | for old_time in self.kill_times: 267 | if current_time - old_time <= KILL_TIME: 268 | kill_count += 1 269 | else: 270 | pop_count += 1 271 | for i in xrange(0, pop_count): 272 | self.kill_times.pop(0) 273 | return kill_count 274 | 275 | def on_kill(self, by, type, grenade): 276 | if by is not None and by is not self: 277 | if type == WEAPON_KILL or type == HEADSHOT_KILL: 278 | by.kill_times.append(reactor.seconds()) 279 | if by.get_kill_count() >= KILL_THRESHOLD: 280 | if KILLS_IN_TIME == BAN: 281 | by.ban('Aimbot detected - kills in time window', KILLS_IN_TIME_BAN_DURATION) 282 | return 283 | elif KILLS_IN_TIME == KICK: 284 | by.kick('Aimbot detected - kills in time window') 285 | return 286 | elif KILLS_IN_TIME == WARN_ADMIN: 287 | current_time = reactor.seconds() 288 | if (current_time - by.kills_in_time_warn_time) > WARN_INTERVAL_MINIMUM: 289 | by.kills_in_time_warn_time = current_time 290 | by.warn_admin() 291 | return connection.on_kill(self, by, type, grenade) 292 | 293 | def multiple_bullets_eject(self): 294 | if MULTIPLE_BULLETS == BAN: 295 | self.ban('Aimbot detected - multiple bullets', MULTIPLE_BULLETS_BAN_DURATION) 296 | elif MULTIPLE_BULLETS == KICK: 297 | self.kick('Aimbot detected - multiple bullets') 298 | elif MULTIPLE_BULLETS == WARN_ADMIN: 299 | current_time = reactor.seconds() 300 | if (current_time - self.multiple_bullets_warn_time) > WARN_INTERVAL_MINIMUM: 301 | self.multiple_bullets_warn_time = current_time 302 | self.warn_admin() 303 | 304 | def on_hit(self, hit_amount, hit_player, type, grenade): 305 | if self.team is not hit_player.team: 306 | if type == WEAPON_KILL or type == HEADSHOT_KILL: 307 | current_time = reactor.seconds() 308 | shotgun_use = False 309 | if current_time - self.shot_time > (0.5 * hit_player.weapon_object.delay): 310 | shotgun_use = True 311 | self.multiple_bullets_count = 0 312 | self.shot_time = current_time 313 | if type == HEADSHOT_KILL: 314 | self.multiple_bullets_count += 1 315 | if self.weapon == RIFLE_WEAPON: 316 | if (not (hit_amount in RIFLE_DAMAGE)) and DETECT_DAMAGE_HACK: 317 | return False 318 | else: 319 | self.rifle_hits += 1 320 | if self.multiple_bullets_count >= RIFLE_MULTIPLE_BULLETS_MAX: 321 | self.multiple_bullets_eject() 322 | return False 323 | elif self.weapon == SMG_WEAPON: 324 | if (not (hit_amount in SMG_DAMAGE)) and DETECT_DAMAGE_HACK: 325 | return False 326 | else: 327 | self.smg_hits += 1 328 | if self.multiple_bullets_count >= SMG_MULTIPLE_BULLETS_MAX: 329 | self.multiple_bullets_eject() 330 | return False 331 | elif self.weapon == SHOTGUN_WEAPON: 332 | if (not (hit_amount in SHOTGUN_DAMAGE)) and DETECT_DAMAGE_HACK: 333 | return False 334 | elif shotgun_use: 335 | self.shotgun_hits += 1 336 | return connection.on_hit(self, hit_amount, hit_player, type, grenade) 337 | 338 | def hit_percent_eject(self, accuracy): 339 | message = 'Aimbot detected - %i%% %s hit accuracy' %\ 340 | (100.0 * accuracy, self.weapon_object.name) 341 | if HIT_PERCENT == BAN: 342 | self.ban(message, HIT_PERCENT_BAN_DURATION) 343 | elif HIT_PERCENT == KICK: 344 | self.kick(message) 345 | elif HIT_PERCENT == WARN_ADMIN: 346 | current_time = reactor.seconds() 347 | if (current_time - self.hit_percent_warn_time) > WARN_INTERVAL_MINIMUM: 348 | self.hit_percent_warn_time = current_time 349 | self.warn_admin() 350 | 351 | def check_percent(self): 352 | if self.weapon == RIFLE_WEAPON: 353 | rifle_perc = float(self.rifle_hits)/float(self.rifle_count) 354 | if self.rifle_count >= RIFLE_KICK_MINIMUM: 355 | if rifle_perc >= RIFLE_KICK_PERC: 356 | self.hit_percent_eject(rifle_perc) 357 | elif self.weapon == SMG_WEAPON: 358 | smg_perc = float(self.smg_hits)/float(self.smg_count) 359 | if self.smg_count >= SMG_KICK_MINIMUM: 360 | if smg_perc >= SMG_KICK_PERC: 361 | self.hit_percent_eject(smg_perc) 362 | elif self.weapon == SHOTGUN_WEAPON: 363 | shotgun_perc = float(self.shotgun_hits)/float(self.shotgun_count) 364 | if self.shotgun_count >= SHOTGUN_KICK_MINIMUM: 365 | if shotgun_perc >= SHOTGUN_KICK_PERC: 366 | self.hit_percent_eject(shotgun_perc) 367 | 368 | def on_bullet_fire(self): 369 | # Remembering the past offers a performance boost, particularly with the SMG 370 | if self.last_target is not None: 371 | if self.last_target.hp is not None: 372 | if self.check_near_miss(self.last_target): 373 | self.check_percent() 374 | return 375 | for enemy in self.possible_targets: 376 | if enemy.hp is not None and enemy is not self.last_target: 377 | if self.check_near_miss(enemy): 378 | self.last_target = enemy 379 | self.check_percent() 380 | return 381 | 382 | def check_near_miss(self, target): 383 | if self.world_object is not None and target.world_object is not None: 384 | p_self = self.world_object.position 385 | p_targ = target.world_object.position 386 | position_v = (p_targ.x - p_self.x, p_targ.y - p_self.y, p_targ.z - p_self.z) 387 | orient = self.world_object.orientation 388 | orient_v = (orient.x, orient.y, orient.z) 389 | position_v_mag = magnitude(position_v) 390 | if position_v_mag != 0 and (dot3d(orient_v, position_v)/position_v_mag) >= NEAR_MISS_COS: 391 | if self.weapon == RIFLE_WEAPON: 392 | self.rifle_count += 1 393 | elif self.weapon == SMG_WEAPON: 394 | self.smg_count += 1 395 | elif self.weapon == SHOTGUN_WEAPON: 396 | self.shotgun_count += 1 397 | return True 398 | return False 399 | 400 | # Data collection stuff 401 | def on_disconnect(self): 402 | self.bullet_loop_stop() 403 | if DATA_COLLECTION: 404 | if self.name != None: 405 | with open('aimbot2log.txt','a') as myfile: 406 | output = self.name.encode('ascii','ignore').replace(',','') + ',' 407 | output += str(self.rifle_hits) + ',' + str(self.rifle_count) + ',' 408 | output += str(self.smg_hits) + ',' + str(self.smg_count) + ',' 409 | output += str(self.shotgun_hits) + ',' + str(self.shotgun_count) + '\n' 410 | myfile.write(output) 411 | myfile.close() 412 | return connection.on_disconnect(self) 413 | 414 | return Aimbot2Protocol, Aimbot2Connection -------------------------------------------------------------------------------- /antispawnkill.py: -------------------------------------------------------------------------------- 1 | """ 2 | Anti-spawnkill. 3 | 4 | Usage: /ask 5 | I'm capping it at 5 by default so people don't accidentally make everyone invincible. 6 | """ 7 | from time import time 8 | from scheduler import Scheduler 9 | from commands import add, admin, name 10 | from pyspades.server import create_player, set_tool, set_color, input_data, weapon_input 11 | from pyspades.common import make_color 12 | 13 | MAX_SECS_NODAMAGE = 5 14 | 15 | @admin 16 | @name('ask') 17 | def antispawnkill(connection, seconds): 18 | protocol = connection.protocol 19 | seconds = int(seconds) 20 | protocol.ask_time = seconds if seconds <= MAX_SECS_NODAMAGE else MAX_SECS_NODAMAGE 21 | connection.protocol.send_chat("Anti-spawnkill time set to %s seconds by %s" % ( protocol.ask_time, connection.name ), irc = True) 22 | 23 | add(antispawnkill) 24 | 25 | # Not the best solution, but it works. Just copied the normal inv function 26 | # and removed anti-kill stuff. 27 | def my_invisible(connection): 28 | protocol = connection.protocol 29 | 30 | player = connection 31 | 32 | player.invisible = not player.invisible 33 | player.filter_visibility_data = player.invisible 34 | 35 | if not player.invisible and player.world_object is not None: 36 | x, y, z = player.world_object.position.get() 37 | create_player.player_id = player.player_id 38 | create_player.name = player.name 39 | create_player.x = x 40 | create_player.y = y 41 | create_player.z = z 42 | create_player.weapon = player.weapon 43 | create_player.team = player.team.id 44 | 45 | world_object = player.world_object 46 | 47 | input_data.player_id = player.player_id 48 | input_data.up = world_object.up 49 | input_data.down = world_object.down 50 | input_data.left = world_object.left 51 | input_data.right = world_object.right 52 | input_data.jump = world_object.jump 53 | input_data.crouch = world_object.crouch 54 | input_data.sneak = world_object.sneak 55 | input_data.sprint = world_object.sprint 56 | 57 | set_tool.player_id = player.player_id 58 | set_tool.value = player.tool 59 | set_color.player_id = player.player_id 60 | set_color.value = make_color(*player.color) 61 | 62 | weapon_input.primary = world_object.primary_fire 63 | weapon_input.secondary = world_object.secondary_fire 64 | 65 | protocol.send_contained(create_player, sender = player, save = True) 66 | protocol.send_contained(set_tool, sender = player) 67 | protocol.send_contained(set_color, sender = player, save = True) 68 | protocol.send_contained(input_data, sender = player) 69 | protocol.send_contained(weapon_input, sender = player) 70 | 71 | player.send_chat("Now visible.") 72 | 73 | def apply_script(protocol, connection, config): 74 | class ASKProtocol(protocol): 75 | def __init__(self, *arg, **kw): 76 | self.ask_time = 0 77 | protocol.__init__(self, *arg, **kw) 78 | 79 | class ASKConnection(connection): 80 | spawn_time = 0 81 | def on_hit(self, hit_amount, hit_player, type, grenade): 82 | if int( time() ) - hit_player.spawn_time < self.protocol.ask_time: 83 | return False 84 | return connection.on_hit(self, hit_amount, hit_player, type, grenade) 85 | 86 | def on_spawn(self, pos): 87 | self.spawn_time = int( time() ) 88 | my_invisible(self) 89 | schedule = Scheduler(self.protocol) 90 | schedule.call_later(self.protocol.ask_time, self.uninvis) 91 | return connection.on_spawn(self, pos) 92 | 93 | def uninvis(self): 94 | my_invisible(self) 95 | 96 | return ASKProtocol, ASKConnection 97 | -------------------------------------------------------------------------------- /arena.py: -------------------------------------------------------------------------------- 1 | # READ THE INSTRUCTIONS BELOW BEFORE YOU ASK QUESTIONS 2 | 3 | # Arena game mode written by Yourself 4 | # A game of team survival. The last team standing scores a point. 5 | 6 | # A map that uses arena needs to be modified to have a starting area for 7 | # each team. A starting area is enclosed and has a gate on it. Each block of a 8 | # gate must have the EXACT same color to work properly. Between each rounds, 9 | # the gate is rebuilt. The gates are destroyed simultaneously at the start of each 10 | # round, releasing the players onto the map. Players are free to switch weapons 11 | # between rounds. 12 | 13 | # Spawn locations and gate locations MUST be present in the map metadata (map txt file) 14 | # for arena to work properly. 15 | 16 | # The spawn location/s for the green team are set by using the data from the 'arena_green_spawns' 17 | # tuple in the extensions dictionary. Likewise, the blue spawn/s is set with the 'arena_blue_spawns' 18 | # key. 'arena_green_spawns' and 'arena_blue_spawns' are tuples which contain tuples of spawn 19 | # coordinates. Spawn locations are chosen randomly. 20 | 21 | # NOTE THAT THE SCRIPT RETAINS BACKWARDS COMPATIBILITY with the old 'arena_green_spawn' and 22 | # 'arena_blue_spawn' 23 | 24 | # The 'arena_max_spawn_distance' can be used to set MAX_SPAWN_DISTANCE on a map by map 25 | # basis. See the comment by MAX_SPAWN_DISTANCE for more information 26 | 27 | # The locations of gates is also determined in the map metadata. 'arena_gates' is a 28 | # tuple of coordinates in the extension dictionary. Each gate needs only one block 29 | # to be specified (since each gate is made of a uniform color) 30 | 31 | # Sample extensions dictionary of an arena map with two gates: 32 | # In this example there is one spawn location for blue and two spawn locations for green. 33 | # extensions = { 34 | # 'arena': True, 35 | # 'arena_blue_spawns' : ((128, 256, 60),), 36 | # 'arena_green_spawns' : ((384, 256, 60), (123, 423, 51)), 37 | # 'arena_gates': ((192, 236, 59), (320, 245, 60)) 38 | # } 39 | 40 | 41 | from pyspades.server import block_action, set_color, block_line 42 | from pyspades import world 43 | from pyspades.constants import * 44 | from twisted.internet import reactor 45 | from twisted.internet.task import LoopingCall 46 | from commands import add, admin 47 | import random 48 | import math 49 | 50 | # If ALWAYS_ENABLED is False, then the 'arena' key must be set to True in 51 | # the 'extensions' dictionary in the map metadata 52 | ALWAYS_ENABLED = True 53 | 54 | # How long should be spent between rounds in arena (seconds) 55 | SPAWN_ZONE_TIME = 15.0 56 | 57 | # How many seconds a team color should be shown after they win a round 58 | # Set to 0 to disable this feature. 59 | TEAM_COLOR_TIME = 4.0 60 | 61 | # Maximum duration that a round can last. Time is in seconds. Set to 0 to 62 | # disable the time limit 63 | MAX_ROUND_TIME = 180 64 | 65 | MAP_CHANGE_DELAY = 25.0 66 | 67 | # Coordinates to hide the tent and the intel 68 | HIDE_COORD = (0, 0, 63) 69 | 70 | # Max distance a player can be from a spawn while the players are held within 71 | # the gates. If they get outside this they are teleported to a spawn. 72 | # Used to teleport players who glitch through the map back into the spawns. 73 | MAX_SPAWN_DISTANCE = 15.0 74 | 75 | BUILDING_ENABLED = False 76 | 77 | if MAX_ROUND_TIME >= 60: 78 | MAX_ROUND_TIME_TEXT = '%.2f minutes' % (float(MAX_ROUND_TIME)/60.0) 79 | else: 80 | MAX_ROUND_TIME_TEXT = str(MAX_ROUND_TIME) + ' seconds' 81 | 82 | @admin 83 | def coord(connection): 84 | connection.get_coord = True 85 | return 'Spade a block to get its coordinate.' 86 | 87 | add(coord) 88 | 89 | def make_color(r, g, b, a = 255): 90 | r = int(r) 91 | g = int(g) 92 | b = int(b) 93 | a = float(a) 94 | return b | (g << 8) | (r << 16) | (int((a / 255.0) * 128.0) << 24) 95 | 96 | # Algorithm for minimizing the number of blocks sent for the gates using 97 | # a block line. Probably won't find the optimal solution for shapes that are not 98 | # rectangular prisms but it's better than nothing. 99 | # d = changing indice 100 | # c1 = first constant indice 101 | # c2 = second constant indice 102 | def partition(points, d, c1, c2): 103 | row = {} 104 | row_list = [] 105 | for point in points: 106 | pc1 = point[c1] 107 | pc2 = point[c2] 108 | if not row.has_key(pc1): 109 | row[pc1] = {} 110 | dic1 = row[pc1] 111 | if not dic1.has_key(pc2): 112 | dic1[pc2] = [] 113 | row_list.append(dic1[pc2]) 114 | dic2 = dic1[pc2] 115 | dic2.append(point) 116 | row_list_sorted = [] 117 | for div in row_list: 118 | row_list_sorted.append(sorted(div, key = lambda k: k[d])) 119 | # row_list_sorted is a list containing lists of points that all have the same 120 | # point[c1] and point[c2] values and are sorted in increasing order according to point[d] 121 | start_block = None 122 | final_blocks = [] 123 | for block_list in row_list_sorted: 124 | counter = 0 125 | for i, block in enumerate(block_list): 126 | counter += 1 127 | if start_block is None: 128 | start_block = block 129 | if i + 1 == len(block_list): 130 | next_block = None 131 | else: 132 | next_block = block_list[i + 1] 133 | # Current AoS version seems to have an upper limit of 65 blocks for a block line 134 | if counter == 65 or next_block is None or block[d] + 1 != next_block[d]: 135 | final_blocks.append([start_block, block]) 136 | start_block = None 137 | counter = 0 138 | return final_blocks 139 | 140 | def minimize_block_line(points): 141 | x = partition(points, 0, 1, 2) 142 | y = partition(points, 1, 0, 2) 143 | z = partition(points, 2, 0, 1) 144 | xlen = len(x) 145 | ylen = len(y) 146 | zlen = len(z) 147 | if xlen <= ylen and xlen <= zlen: 148 | return x 149 | if ylen <= xlen and ylen <= zlen: 150 | return y 151 | if zlen <= xlen and zlen <= ylen: 152 | return z 153 | return x 154 | 155 | def get_team_alive_count(team): 156 | count = 0 157 | for player in team.get_players(): 158 | if not player.world_object.dead: 159 | count += 1 160 | return count 161 | 162 | def get_team_dead(team): 163 | for player in team.get_players(): 164 | if not player.world_object.dead: 165 | return False 166 | return True 167 | 168 | class CustomException(Exception): 169 | def __init__(self, value): 170 | self.parameter = value 171 | 172 | def __str__(self): 173 | return repr(self.parameter) 174 | 175 | class Gate: 176 | def __init__(self, x, y, z, protocol_obj): 177 | self.support_blocks = [] 178 | self.blocks = [] 179 | self.protocol_obj = protocol_obj 180 | map = self.protocol_obj.map 181 | solid, self.color = map.get_point(x, y, z) 182 | if not solid: 183 | raise CustomException('The gate coordinate (%i, %i, %i) is not solid.' % (x, y, z)) 184 | self.record_gate(x, y, z) 185 | self.blocks = minimize_block_line(self.blocks) 186 | 187 | def build_gate(self): 188 | map = self.protocol_obj.map 189 | set_color.value = make_color(*self.color) 190 | set_color.player_id = block_line.player_id = 32 191 | self.protocol_obj.send_contained(set_color, save = True) 192 | for block_line_ in self.blocks: 193 | start_block, end_block = block_line_ 194 | points = world.cube_line(*(start_block + end_block)) 195 | if not points: 196 | continue 197 | for point in points: 198 | x, y, z = point 199 | if not map.get_solid(x, y, z): 200 | map.set_point(x, y, z, self.color) 201 | block_line.x1, block_line.y1, block_line.z1 = start_block 202 | block_line.x2, block_line.y2, block_line.z2 = end_block 203 | self.protocol_obj.send_contained(block_line, save = True) 204 | 205 | def destroy_gate(self): 206 | map = self.protocol_obj.map 207 | block_action.player_id = 32 208 | block_action.value = DESTROY_BLOCK 209 | for block in self.support_blocks: # optimize wire traffic 210 | if map.get_solid(*block): 211 | map.remove_point(*block) 212 | block_action.x, block_action.y, block_action.z = block 213 | self.protocol_obj.send_contained(block_action, save = True) 214 | for block_line_ in self.blocks: # avoid desyncs 215 | start_block, end_block = block_line_ 216 | points = world.cube_line(*(start_block + end_block)) 217 | if not points: 218 | continue 219 | for point in points: 220 | x, y, z = point 221 | if map.get_solid(x, y, z): 222 | map.remove_point(x, y, z) 223 | 224 | def record_gate(self, x, y, z): 225 | if x < 0 or x > 511 or y < 0 or x > 511 or z < 0 or z > 63: 226 | return False 227 | solid, color = self.protocol_obj.map.get_point(x, y, z) 228 | if solid: 229 | coordinate = (x, y, z) 230 | if color[0] != self.color[0] or color[1] != self.color[1] or color[2] != self.color[2]: 231 | return True 232 | for block in self.blocks: 233 | if coordinate == block: 234 | return False 235 | self.blocks.append(coordinate) 236 | returns = (self.record_gate(x+1, y, z), 237 | self.record_gate(x-1, y, z), 238 | self.record_gate(x, y+1, z), 239 | self.record_gate(x, y-1, z), 240 | self.record_gate(x, y, z+1), 241 | self.record_gate(x, y, z-1)) 242 | if True in returns: 243 | self.support_blocks.append(coordinate) 244 | return False 245 | 246 | def apply_script(protocol, connection, config): 247 | class ArenaConnection(connection): 248 | get_coord = False 249 | 250 | def on_block_destroy(self, x, y, z, mode): 251 | returned = connection.on_block_destroy(self, x, y, z, mode) 252 | if self.get_coord: 253 | self.get_coord = False 254 | self.send_chat('Coordinate: %i, %i, %i' % (x, y, z)) 255 | return False 256 | return returned 257 | 258 | def on_disconnect(self): 259 | if self.protocol.arena_running: 260 | if self.world_object is not None: 261 | self.world_object.dead = True 262 | self.protocol.check_round_end() 263 | return connection.on_disconnect(self) 264 | 265 | def on_kill(self, killer, type, grenade): 266 | if self.protocol.arena_running and type != TEAM_CHANGE_KILL: 267 | if self.world_object is not None: 268 | self.world_object.dead = True 269 | self.protocol.check_round_end(killer) 270 | return connection.on_kill(self, killer, type, grenade) 271 | 272 | def on_team_join(self, team): 273 | returned = connection.on_team_join(self, team) 274 | if returned is False: 275 | return False 276 | if self.protocol.arena_running: 277 | if self.world_object is not None and not self.world_object.dead: 278 | self.world_object.dead = True 279 | self.protocol.check_round_end() 280 | return returned 281 | 282 | def on_position_update(self): 283 | if not self.protocol.arena_running: 284 | min_distance = None 285 | pos = self.world_object.position 286 | for spawn in self.team.arena_spawns: 287 | xd = spawn[0] - pos.x 288 | yd = spawn[1] - pos.y 289 | zd = spawn[2] - pos.z 290 | distance = math.sqrt(xd ** 2 + yd ** 2 + zd ** 2) 291 | if min_distance is None or distance < min_distance: 292 | min_distance = distance 293 | if min_distance > self.protocol.arena_max_spawn_distance: 294 | self.set_location(random.choice(self.team.arena_spawns)) 295 | self.refill() 296 | return connection.on_position_update(self) 297 | 298 | def get_respawn_time(self): 299 | if self.protocol.arena_enabled: 300 | if self.protocol.arena_running: 301 | return -1 302 | else: 303 | return 1 304 | return connection.get_respawn_time(self); 305 | 306 | def respawn(self): 307 | if self.protocol.arena_running: 308 | return False 309 | return connection.respawn(self) 310 | 311 | def on_spawn(self, pos): 312 | returned = connection.on_spawn(self, pos) 313 | if self.protocol.arena_running: 314 | self.kill() 315 | return returned 316 | 317 | def on_spawn_location(self, pos): 318 | if self.protocol.arena_enabled: 319 | return random.choice(self.team.arena_spawns) 320 | return connection.on_spawn_location(self, pos) 321 | 322 | def on_flag_take(self): 323 | if self.protocol.arena_take_flag: 324 | self.protocol.arena_take_flag = False 325 | return connection.on_flag_take(self) 326 | return False 327 | 328 | def on_refill(self): 329 | returned = connection.on_refill(self) 330 | if self.protocol.arena_running: 331 | return False 332 | return returned 333 | 334 | class ArenaProtocol(protocol): 335 | old_respawn_time = None 336 | old_building = None 337 | old_killing = None 338 | arena_enabled = False 339 | arena_running = False 340 | arena_counting_down = False 341 | arena_take_flag = False 342 | arena_countdown_timers = None 343 | arena_limit_timer = None 344 | arena_old_fog_color = None 345 | arena_max_spawn_distance = MAX_SPAWN_DISTANCE 346 | 347 | def check_round_end(self, killer = None, message = True): 348 | if not self.arena_running: 349 | return 350 | for team in (self.green_team, self.blue_team): 351 | if get_team_dead(team): 352 | self.arena_win(team.other, killer) 353 | return 354 | if message: 355 | self.arena_remaining_message() 356 | 357 | def arena_time_limit(self): 358 | self.arena_limit_timer = None 359 | green_team = self.green_team 360 | blue_team = self.blue_team 361 | green_count = get_team_alive_count(green_team) 362 | blue_count = get_team_alive_count(blue_team) 363 | if green_count > blue_count: 364 | self.arena_win(green_team) 365 | elif green_count < blue_count: 366 | self.arena_win(blue_team) 367 | else: 368 | self.send_chat('Round ends in a tie.') 369 | self.begin_arena_countdown() 370 | 371 | def arena_win(self, team, killer = None): 372 | if not self.arena_running: 373 | return 374 | if self.arena_old_fog_color is None and TEAM_COLOR_TIME > 0: 375 | self.arena_old_fog_color = self.fog_color 376 | self.set_fog_color(team.color) 377 | reactor.callLater(TEAM_COLOR_TIME, self.arena_reset_fog_color) 378 | if killer is None or killer.team is not team: 379 | for player in team.get_players(): 380 | if not player.world_object.dead: 381 | killer = player 382 | break 383 | if killer is not None: 384 | self.arena_take_flag = True 385 | killer.take_flag() 386 | killer.capture_flag() 387 | self.send_chat(team.name + ' team wins the round!') 388 | self.begin_arena_countdown() 389 | 390 | def arena_reset_fog_color(self): 391 | if self.arena_old_fog_color is not None: 392 | # Shitty fix for disco on game end 393 | self.old_fog_color = self.arena_old_fog_color 394 | self.set_fog_color(self.arena_old_fog_color) 395 | self.arena_old_fog_color = None 396 | 397 | def arena_remaining_message(self): 398 | if not self.arena_running: 399 | return 400 | green_team = self.green_team 401 | blue_team = self.blue_team 402 | for team in (self.green_team, self.blue_team): 403 | num = get_team_alive_count(team) 404 | team.arena_message = '%i player' % num 405 | if num != 1: 406 | team.arena_message += 's' 407 | team.arena_message += ' on ' + team.name 408 | self.send_chat('%s and %s remain.' % (green_team.arena_message, blue_team.arena_message)) 409 | 410 | def on_map_change(self, map): 411 | extensions = self.map_info.extensions 412 | if ALWAYS_ENABLED: 413 | self.arena_enabled = True 414 | else: 415 | if extensions.has_key('arena'): 416 | self.arena_enabled = extensions['arena'] 417 | else: 418 | self.arena_enabled = False 419 | self.arena_max_spawn_distance = MAX_SPAWN_DISTANCE 420 | if self.arena_enabled: 421 | self.old_respawn_time = self.respawn_time 422 | self.respawn_time = 0 423 | self.old_building = self.building 424 | self.old_killing = self.killing 425 | self.gates = [] 426 | if extensions.has_key('arena_gates'): 427 | for gate in extensions['arena_gates']: 428 | self.gates.append(Gate(*gate, protocol_obj=self)) 429 | if extensions.has_key('arena_green_spawns'): 430 | self.green_team.arena_spawns = extensions['arena_green_spawns'] 431 | elif extensions.has_key('arena_green_spawn'): 432 | self.green_team.arena_spawns = (extensions['arena_green_spawn'],) 433 | else: 434 | raise CustomException('No arena_green_spawns given in map metadata.') 435 | if extensions.has_key('arena_blue_spawns'): 436 | self.blue_team.arena_spawns = extensions['arena_blue_spawns'] 437 | elif extensions.has_key('arena_blue_spawn'): 438 | self.blue_team.arena_spawns = (extensions['arena_blue_spawn'],) 439 | else: 440 | raise CustomException('No arena_blue_spawns given in map metadata.') 441 | if extensions.has_key('arena_max_spawn_distance'): 442 | self.arena_max_spawn_distance = extensions['arena_max_spawn_distance'] 443 | self.delay_arena_countdown(MAP_CHANGE_DELAY) 444 | self.begin_arena_countdown() 445 | else: 446 | # Cleanup after a map change 447 | if self.old_respawn_time is not None: 448 | self.respawn_time = self.old_respawn_time 449 | if self.old_building is not None: 450 | self.building = self.old_building 451 | if self.old_killing is not None: 452 | self.killing = self.old_killing 453 | self.arena_enabled = False 454 | self.arena_running = False 455 | self.arena_counting_down = False 456 | self.arena_limit_timer = None 457 | self.arena_old_fog_color = None 458 | self.old_respawn_time = None 459 | self.old_building = None 460 | self.old_killing = None 461 | return protocol.on_map_change(self, map) 462 | 463 | def build_gates(self): 464 | for gate in self.gates: 465 | gate.build_gate() 466 | 467 | def destroy_gates(self): 468 | for gate in self.gates: 469 | gate.destroy_gate() 470 | 471 | def arena_spawn(self): 472 | for player in self.players.values(): 473 | if player.team.spectator: 474 | continue 475 | if player.world_object.dead: 476 | player.spawn(random.choice(player.team.arena_spawns)) 477 | else: 478 | player.set_location(random.choice(player.team.arena_spawns)) 479 | player.refill() 480 | 481 | def begin_arena_countdown(self): 482 | if self.arena_limit_timer is not None: 483 | if self.arena_limit_timer.cancelled == 0 and self.arena_limit_timer.called == 0: 484 | self.arena_limit_timer.cancel() 485 | self.arena_limit_timer = None 486 | if self.arena_counting_down: 487 | return 488 | self.arena_running = False 489 | self.arena_limit_timer = None 490 | self.arena_counting_down = True 491 | self.killing = False 492 | self.building = False 493 | self.build_gates() 494 | self.arena_spawn() 495 | self.send_chat('The round will begin in %i seconds.' % SPAWN_ZONE_TIME) 496 | self.arena_countdown_timers = [reactor.callLater(SPAWN_ZONE_TIME, self.begin_arena)] 497 | for time in xrange(1, 6): 498 | self.arena_countdown_timers.append(reactor.callLater(SPAWN_ZONE_TIME - time, self.send_chat, str(time))) 499 | 500 | def delay_arena_countdown(self, amount): 501 | if self.arena_counting_down: 502 | for timer in self.arena_countdown_timers: 503 | if timer.cancelled == 0 and timer.called == 0: 504 | timer.delay(amount) 505 | 506 | def begin_arena(self): 507 | self.arena_counting_down = False 508 | for team in (self.green_team, self.blue_team): 509 | if team.count() == 0: 510 | self.send_chat('Not enough players on the %s team to begin.' % team.name) 511 | self.begin_arena_countdown() 512 | return 513 | self.arena_running = True 514 | self.killing = True 515 | self.building = BUILDING_ENABLED 516 | self.destroy_gates() 517 | self.send_chat('Go!') 518 | if MAX_ROUND_TIME > 0: 519 | self.send_chat('There is a time limit of %s for this round.' % MAX_ROUND_TIME_TEXT) 520 | self.arena_limit_timer = reactor.callLater(MAX_ROUND_TIME, self.arena_time_limit) 521 | 522 | def on_base_spawn(self, x, y, z, base, entity_id): 523 | if not self.arena_enabled: 524 | return protocol.on_base_spawn(self, x, y, z, base, entity_id) 525 | return HIDE_COORD 526 | 527 | def on_flag_spawn(self, x, y, z, flag, entity_id): 528 | if not self.arena_enabled: 529 | return protocol.on_base_spawn(self, x, y, z, flag, entity_id) 530 | return HIDE_COORD 531 | 532 | return ArenaProtocol, ArenaConnection 533 | -------------------------------------------------------------------------------- /avx.py: -------------------------------------------------------------------------------- 1 | # avx.py 2 | import array 3 | import collections 4 | import operator 5 | import math 6 | import io 7 | import gzip 8 | import os 9 | 10 | from itertools import izip, imap, chain, ifilter, product, repeat 11 | from struct import pack, unpack, calcsize 12 | 13 | # this module is a self-contained pure python implementation of a generic AVX loader/saver 14 | 15 | # AVX Header: magic "AVX" followed by a version byte. Then a version-specific header. 16 | # Depending on the version and header, it will load fixed or variable sized voxel geometry 17 | # and optionally color data for surface voxels. 18 | 19 | # A voxel is surface IFF: 20 | # it is solid AND (one of its neighbors is not solid OR it is on the edge) 21 | 22 | # Note: This is probably a better implementation of bitarrays: http://pypi.python.org/pypi/bitarray#downloads 23 | 24 | DEFAULT_COLOR = (103, 64, 40) 25 | 26 | class BitArray(object): 27 | _bits = 8 28 | _maxbit = _bits - 1 29 | _max = 2 ** _bits - 1 30 | _log = int(round(math.log(_bits, 2))) 31 | 32 | def __init__(self, bits, fill = 0): 33 | self.bits = int(bits) 34 | self.bit_array = array.array('B') 35 | 36 | if fill == 1: 37 | fill = self._max # all bits set 38 | else: 39 | fill = 0 # all bits cleared 40 | 41 | self.bit_array.extend((fill,) * self._array_size(self.bits)) 42 | 43 | @classmethod 44 | def fromstring(cls, str, bits = -1): 45 | ret = cls(0) 46 | ret.loadstring(str, bits) 47 | return ret 48 | 49 | def loadstring(self, str, bits = -1): 50 | max_bits = len(str) * 8 51 | if bits > max_bits: 52 | raise ValueError() 53 | if bits < max_bits: 54 | str = str[:int(math.ceil(bits/8.0))] 55 | self.bit_array.fromstring(str) 56 | self.bits = max(bits, max_bits) 57 | 58 | @staticmethod 59 | def _array_size(bits): 60 | i = bits >> BitArray._log 61 | if (bits & BitArray._maxbit): 62 | i += 1 # a record for stragglers 63 | return i 64 | 65 | def get(self, bit_num): 66 | record = bit_num >> self._log 67 | offset = bit_num & self._maxbit 68 | mask = 1 << offset 69 | return (self.bit_array[record] & mask) >> offset 70 | 71 | def set(self, bit_num): 72 | record = bit_num >> self._log 73 | offset = bit_num & self._maxbit 74 | mask = 1 << offset 75 | self.bit_array[record] |= mask 76 | 77 | def clear(self, bit_num): 78 | record = bit_num >> self._log 79 | offset = bit_num & self._maxbit 80 | mask = ~(1 << offset) 81 | self.bit_array[record] &= mask 82 | 83 | def toggle(self, bit_num): 84 | record = bit_num >> self._log 85 | offset = bit_num & self._maxbit 86 | mask = 1 << offset 87 | self.bit_array[record] ^= mask 88 | 89 | def tostring(self, padbytes = 1): 90 | # minimum padbytes == 1 91 | str = self.bit_array.tostring() 92 | str = str[:int(math.ceil(self.bits / 8.0))] 93 | str += '\x00' * (-len(str) % padbytes) 94 | return str 95 | 96 | class BitArrayND(BitArray): 97 | def __init__(self, shape, fill=0): 98 | self.shape = shape 99 | BitArray.__init__(self, self.bits, fill) 100 | 101 | bits = property(lambda self: reduce(operator.mul, self.shape), lambda self, value: None) 102 | 103 | @classmethod 104 | def fromsparselist(cls, list): 105 | ret = cls((0,) * len(list[0])) 106 | ret.shape = [n+1 for n in map(max, izip(*list))] 107 | ret.bit_array.extend((0,) * ret._array_size(ret.bits)) 108 | for coords in list: 109 | ret.set(coords) 110 | return ret 111 | 112 | @classmethod 113 | def fromstring(cls, shape, str): 114 | ret = cls((0,) * len(shape)) 115 | ret.shape = shape 116 | BitArray.loadstring(ret, str, ret.bits) 117 | return ret 118 | 119 | def _ravel(self, coords): 120 | i = 0 121 | for dim, j in zip(self.shape, coords): 122 | i = i * dim + j 123 | return i 124 | 125 | def get(self, coords): 126 | return BitArray.get(self, self._ravel(coords)) 127 | 128 | def set(self, coords): 129 | return BitArray.set(self, self._ravel(coords)) 130 | 131 | def clear(self, coords): 132 | return BitArray.clear(self, self._ravel(coords)) 133 | 134 | def toggle(self, coords): 135 | return BitArray.toggle(self, self._ravel(coords)) 136 | 137 | def tosparselist(self): 138 | ret = [] 139 | for coords in product(*map(xrange, self.shape)): 140 | if self.get(coords): 141 | ret.append(coords) 142 | return ret 143 | 144 | def isvalidcoords(self, coords): 145 | return all((n >= 0 and n < d for n, d in izip(coords, self.shape))) 146 | 147 | def neighbors(self, coords): 148 | 'returns the coordinates of all the valid elements whose coordinates differ from `coords` by +-1 in any one dimension' 149 | if not self.isvalidcoords(coords): 150 | return 151 | i = 0 152 | for changed in map(sum,product(coords, (1, -1))): 153 | n = coords[:i//2] + (changed,) + coords[i//2+1:] 154 | if self.isvalidcoords(n): 155 | yield n 156 | i += 1 157 | 158 | def open_gzip(file = None, fileobj = None): 159 | if fileobj is None: 160 | if not os.path.isfile(file) and os.path.isfile(file + '.gz'): 161 | file += '.gz' 162 | return open_gzip(fileobj = open(file, 'rb')) 163 | p = fileobj.tell() 164 | magic = unpack('H', fileobj.read(2)) 165 | fileobj.seek(p, 0) 166 | if magic == 0x1F8B: # .gz magic 167 | fileobj = gzip.GzipFile(fileobj = fileobj) 168 | return fileobj 169 | 170 | class AVX(BitArrayND): 171 | # headers [(attribute_name, struct.fmt)] 172 | avx_magic = [('magic', '3s'), ('ver', 'B')] 173 | avx_headers_ver = [ 174 | [('size_x', 'H'), ('size_y', 'H'), ('size_z', 'H'), ('has_colors', '?'), ('pad_bytes', 'B')] 175 | ] 176 | 177 | magic = 'AVX' 178 | ver = 0 179 | 180 | def __init__(self, x, y, z, colored = True, default_color = DEFAULT_COLOR): 181 | BitArrayND.__init__(self, [x, y, z]) 182 | self.has_colors = bool(colored) 183 | self.colors = dict() 184 | self.default_color = tuple(default_color) 185 | self.pad_bytes = 1 186 | 187 | @classmethod 188 | def fromsparselist(cls, list, colored = False, default_color = DEFAULT_COLOR): 189 | # a list of 1 bits coords in the form of [(x1,y1,z1), (x2,y2,z2)] 190 | parent = BitArrayND.fromsparselist(list) 191 | ret = cls(0, 0, 0, colored = colored, default_color = default_color) 192 | ret.shape = parent.shape 193 | ret.bit_array = parent.bit_array 194 | if ret.has_colors: 195 | ret.colors = dict((xyz, ret.default_color) for xyz in product(*map(xrange, ret.shape)) if ret.issurface(xyz)) 196 | return ret 197 | 198 | @classmethod 199 | def fromsparsedict(cls, dict, colored = True, default_color = DEFAULT_COLOR): 200 | # {(x1,y1,z1): color, (x2,y2,z2): None, ...} 201 | ret = cls.fromsparselist(dict.keys(), colored = colored, default_color = default_color) 202 | if ret.has_colors: 203 | for coords, color in dict.iteritems(): 204 | ret.setcolor(coords, color) 205 | return ret 206 | 207 | @classmethod 208 | def fromfile(cls, file = None, fileobj = None): 209 | fileobj = open_gzip(file, fileobj) 210 | 211 | # new instance, load magic attributes 212 | ret = cls(0, 0, 0) 213 | ret._load_attributes(fileobj, cls.avx_magic) 214 | 215 | if ret.magic != cls.magic or ret.ver > cls.ver: 216 | raise IOError("Not an AVX file") 217 | 218 | ret._load_attributes(fileobj, ret.avx_headers_ver[ret.ver]) 219 | 220 | bytes = int(math.ceil(ret.bits/8.0)) 221 | bytes += -bytes % ret.pad_bytes 222 | ret.loadstring(fileobj.read(bytes), ret.bits) 223 | 224 | if ret.has_colors: 225 | #read at most x*y*z color tuples 226 | str = fileobj.read(3*reduce(operator.mul, ret.shape)) 227 | i = 0 228 | for xyz in product(*map(xrange, ret.shape)): 229 | if ret.issurface(xyz): 230 | ret.colors[xyz] = unpack('BBB', str[i:i+3]) 231 | i += 3 232 | 233 | return ret 234 | 235 | def _load_attributes(self, fileobj, attributes): 236 | # save the current position, seek to the end to get remaining size, seek back 237 | pos = fileobj.tell() 238 | fileobj.seek(0, 2) 239 | size = fileobj.tell() 240 | fileobj.seek(pos, 0) 241 | if size - pos < calcsize(''.join(zip(*attributes)[1])): 242 | raise EOFError("Incomplete AVX file.") 243 | 244 | for attr, fmt in attributes: 245 | setattr(self, attr, unpack(fmt, fileobj.read(calcsize(fmt)))[0]) 246 | 247 | def save(self, file = None, fileobj = None, compresslevel = None): 248 | if fileobj is None: 249 | return self.save(fileobj = open(file, 'wb')) 250 | if compresslevel: 251 | return self.save(fileobj = GzipFile(fileobj = fileobj, compresslevel = compresslevel)) 252 | 253 | for attr, fmt in chain(self.avx_magic, self.avx_headers_ver[self.ver]): 254 | fileobj.write(pack(fmt, getattr(self, attr))) 255 | 256 | fileobj.write(self.tostring(self.pad_bytes)) 257 | 258 | if self.has_colors: 259 | for xyz in sorted(self.colors): 260 | fileobj.write(pack('BBB', *self.colors[xyz])) 261 | 262 | def props(n): 263 | def get(self): return self.shape[n] 264 | def set(self, value): self.shape[n] = value 265 | return get, set 266 | size_x, size_y, size_z = [property(*props(n)) for n in xrange(3)] 267 | del props 268 | 269 | def tosparsedict(self): 270 | return dict((coords, self.colors.get(coords, None)) for coords in self.tosparselist()) 271 | 272 | def setcolor(self, coords, color): 273 | if self.has_colors and self.issurface(coords): 274 | self.colors[coords] = color 275 | 276 | def getcolor(self, coords): 277 | if self.has_colors and self.issurface(coords): 278 | return self.colors(coords) 279 | 280 | def fixcolors(fn): 281 | def wrapper(self, coords): 282 | fn(self, coords) 283 | for coord in list(self.neighbors(coords)) + [coords]: 284 | c = self.colors.has_key(coord) 285 | s = self.issurface(coord) 286 | if c != s: 287 | if c: 288 | self.colors.discard(coord) 289 | else: 290 | self.colors[coord] = self.default_color 291 | 292 | return wrapper 293 | 294 | @fixcolors 295 | def set(self, coords): 296 | BitArrayND.set(self, coords) 297 | 298 | @fixcolors 299 | def clear(self, coords): 300 | BitArrayND.clear(self, coords) 301 | 302 | @fixcolors 303 | def toggle(self, coords): 304 | BitArrayND.toggle(self, coords) 305 | 306 | del fixcolors 307 | 308 | def issurface(self, coords): 309 | return self.get(coords) and ( 310 | any(a == 0 or a == n-1 for a,n in izip(coords, self.shape)) # on the edge of the map 311 | or not all(imap(self.get, self.neighbors(coords)))) # one of it neighbors is missing 312 | # -------------------------------------------------------------------------------- /babel.py: -------------------------------------------------------------------------------- 1 | # Tower of Babel created by Yourself, modified by izzy 2 | # how to install: 3 | # set game_mode to ctf in config.txt 4 | # add babel to script list in config.txt 5 | # add to map .txt files: extensions = { 'babel' : True } 6 | # additional suggestions: 7 | # add onectf to script list in config.txt http://pyspades.googlecode.com/hg/contrib/scripts/onectf.py 8 | # set cap_limit to 10 in config.txt 9 | 10 | from pyspades.constants import * 11 | from random import randint 12 | 13 | # If ALWAYS_ENABLED is False, then babel can be enabled by setting 'babel': True 14 | # in the map metadat extensions dictionary. 15 | ALWAYS_ENABLED = True 16 | 17 | PLATFORM_WIDTH = 100 18 | PLATFORM_HEIGHT = 32 19 | PLATFORM_COLOR = (255, 255, 255, 255) 20 | BLUE_BASE_COORDS = (256-138, 256) 21 | GREEN_BASE_COORDS = (256+138, 256) 22 | SPAWN_SIZE = 40 23 | 24 | 25 | # Don't touch this stuff 26 | PLATFORM_WIDTH /= 2 27 | PLATFORM_HEIGHT /= 2 28 | SPAWN_SIZE /= 2 29 | 30 | def get_entity_location(self, entity_id): 31 | if entity_id == BLUE_BASE: 32 | return BLUE_BASE_COORDS + (self.protocol.map.get_z(*BLUE_BASE_COORDS),) 33 | elif entity_id == GREEN_BASE: 34 | return GREEN_BASE_COORDS + (self.protocol.map.get_z(*GREEN_BASE_COORDS),) 35 | elif entity_id == BLUE_FLAG: 36 | return (256 - PLATFORM_WIDTH + 1, 256, 0) 37 | elif entity_id == GREEN_FLAG: 38 | return (256 + PLATFORM_WIDTH - 1, 256, 0) 39 | 40 | def get_spawn_location(connection): 41 | xb = connection.team.base.x 42 | yb = connection.team.base.y 43 | xb += randint(-SPAWN_SIZE, SPAWN_SIZE) 44 | yb += randint(-SPAWN_SIZE, SPAWN_SIZE) 45 | return (xb, yb, connection.protocol.map.get_z(xb, yb)) 46 | 47 | def coord_on_platform(x, y, z): 48 | if z <= 2: 49 | if x >= (256 - PLATFORM_WIDTH) and x <= (256 + PLATFORM_WIDTH) and y >= (256 - PLATFORM_HEIGHT) and y <= (256 + PLATFORM_HEIGHT): 50 | return True 51 | if z == 1: 52 | if x >= (256 - PLATFORM_WIDTH - 1) and x <= (256 + PLATFORM_WIDTH + 1) \ 53 | and y >= (256 - PLATFORM_HEIGHT - 1) and y <= (256 + PLATFORM_HEIGHT + 1): 54 | return True 55 | return False 56 | 57 | def apply_script(protocol, connection, config): 58 | class BabelProtocol(protocol): 59 | babel = False 60 | def on_map_change(self, map): 61 | extensions = self.map_info.extensions 62 | if ALWAYS_ENABLED: 63 | self.babel = True 64 | else: 65 | if extensions.has_key('babel'): 66 | self.babel = extensions['babel'] 67 | else: 68 | self.babel = False 69 | if self.babel: 70 | self.map_info.cap_limit = 1 71 | self.map_info.get_entity_location = get_entity_location 72 | self.map_info.get_spawn_location = get_spawn_location 73 | for x in xrange(256 - PLATFORM_WIDTH, 256 + PLATFORM_WIDTH): 74 | for y in xrange(256 - PLATFORM_HEIGHT, 256 + PLATFORM_HEIGHT): 75 | map.set_point(x, y, 1, PLATFORM_COLOR) 76 | return protocol.on_map_change(self, map) 77 | 78 | def is_indestructable(self, x, y, z): 79 | if self.babel: 80 | if coord_on_platform(x, y, z): 81 | protocol.is_indestructable(self, x, y, z) 82 | return True 83 | return protocol.is_indestructable(self, x, y, z) 84 | 85 | class BabelConnection(connection): 86 | def invalid_build_position(self, x, y, z): 87 | if not self.god and self.protocol.babel: 88 | if coord_on_platform(x, y, z): 89 | connection.on_block_build_attempt(self, x, y, z) 90 | return True 91 | # prevent enemies from building in protected areas 92 | if self.team is self.protocol.blue_team: 93 | if self.world_object.position.x >= 301 and self.world_object.position.x <= 384 \ 94 | and self.world_object.position.y >= 240 and self.world_object.position.y <= 272: 95 | self.send_chat('You can\'t build near the enemy\'s tower!') 96 | return True 97 | if self.team is self.protocol.green_team: 98 | if self.world_object.position.x >= 128 and self.world_object.position.x <= 211 \ 99 | and self.world_object.position.y >= 240 and self.world_object.position.y <= 272: 100 | self.send_chat('You can\'t build near the enemy\'s tower!') 101 | return True 102 | return False 103 | 104 | def on_block_build_attempt(self, x, y, z): 105 | if self.invalid_build_position(x, y, z): 106 | return False 107 | return connection.on_block_build_attempt(self, x, y, z) 108 | 109 | def on_line_build_attempt(self, points): 110 | for point in points: 111 | if self.invalid_build_position(*point): 112 | return False 113 | return connection.on_line_build_attempt(self, points) 114 | 115 | # anti team destruction 116 | def on_block_destroy(self, x, y, z, mode): 117 | if self.team is self.protocol.blue_team: 118 | if self.tool is SPADE_TOOL and self.world_object.position.x >= 128 and self.world_object.position.x <= 211 \ 119 | and self.world_object.position.y >= 240 and self.world_object.position.y <= 272: 120 | self.send_chat('You can\'t destroy your team\'s blocks in this area. Attack the enemy\'s tower!') 121 | return False 122 | if self.world_object.position.x <= 288: 123 | if self.tool is WEAPON_TOOL: 124 | self.send_chat('You must be closer to the enemy\'s base to shoot blocks!') 125 | return False 126 | if self.tool is GRENADE_TOOL: 127 | self.send_chat('You must be closer to the enemy\'s base to grenade blocks!') 128 | return False 129 | if self.team is self.protocol.green_team: 130 | if self.tool is SPADE_TOOL and self.world_object.position.x >= 301 and self.world_object.position.x <= 384 \ 131 | and self.world_object.position.y >= 240 and self.world_object.position.y <= 272: 132 | self.send_chat('You can\'t destroy your team\'s blocks in this area. Attack the enemy\'s tower!') 133 | return False 134 | if self.world_object.position.x >= 224: 135 | if self.tool is WEAPON_TOOL: 136 | self.send_chat('You must be closer to the enemy\'s base to shoot blocks!') 137 | return False 138 | if self.tool is GRENADE_TOOL: 139 | self.send_chat('You must be closer to the enemy\'s base to grenade blocks!') 140 | return False 141 | return connection.on_block_destroy(self, x, y, z, mode) 142 | 143 | return BabelProtocol, BabelConnection 144 | -------------------------------------------------------------------------------- /badmin.py: -------------------------------------------------------------------------------- 1 | ###Badmin is an bot admin. He'll do a variety of common admin tasks so you don't have to. 2 | ###He might not always get it right, but he'll get it done, and isn't that what really matters? 3 | ###-Requirements: blockinfo.py (for grief detection), ratio.py (for k/d ratio), aimbot2.py (hit accuracy) 4 | 5 | from twisted.internet import reactor 6 | from pyspades.common import prettify_timespan 7 | from pyspades.constants import * 8 | from pyspades.collision import distance_3d_vector 9 | from commands import add, admin, get_player 10 | import re 11 | 12 | BADMIN_VERSION = 9 13 | #Settings for auto-aimbot 14 | SCORE_AIMBOT_ENABLED = True 15 | #any votekicks under uncertain will be cancelled 16 | SCORE_AIMBOT_UNCERTAIN = 2 17 | SCORE_AIMBOT_WARN = 5 18 | SCORE_AIMBOT_KICK = 15 19 | SCORE_AIMBOT_BAN = 25 20 | 21 | #Settings for auto-griefcheck 22 | SCORE_GRIEF_ENABLED = True 23 | #any votekicks under uncertain will be cancelled 24 | SCORE_GRIEF_UNCERTAIN = 2 25 | SCORE_GRIEF_WARN = 4 26 | SCORE_GRIEF_KICK = 8 27 | SCORE_GRIEF_BAN = 12 28 | 29 | #Settings for blank reason votekicks 30 | #turns on setting preventing blank votekicks 31 | BLANK_VOTEKICK_ENABLED = True 32 | 33 | #Settings for language filter 34 | LANGUAGE_FILTER_ENABLED = True 35 | 36 | 37 | slur_pattern = re.compile(".*nigger.*", re.IGNORECASE) 38 | grief_pattern = re.compile(".*(gr.*f.*(ing|er)|grief|destroy).*", re.IGNORECASE) 39 | aimbot_pattern = re.compile(".*(aim|bot|ha(ck|x)|cheat).*", re.IGNORECASE) 40 | 41 | def slur_match(player, msg): 42 | return (not slur_pattern.match(msg) is None) 43 | 44 | def grief_match(player, msg): 45 | return (not grief_pattern.match(msg) is None) 46 | 47 | def aimbot_match(player, msg): 48 | return (not aimbot_pattern.match(msg) is None) 49 | 50 | @admin 51 | def badmin(connection, var=None): 52 | if var == None: 53 | return ("@Badmin (r%s): Language Filter(LF) [%s], Blank Votekick Blocker(BV) " 54 | "[%s], Grief Votekick Protection(GV) [%s], Aimbot Votekick Protection(AV) [%s]" 55 | % (BADMIN_VERSION, LANGUAGE_FILTER_ENABLED, BLANK_VOTEKICK_ENABLED, 56 | SCORE_GRIEF_ENABLED, SCORE_AIMBOT_ENABLED)) 57 | add(badmin) 58 | 59 | @admin 60 | def investigate(connection, player): 61 | player = get_player(connection.protocol, player) 62 | score = score_grief(connection, player) 63 | kdr = round(player.ratio_kills/float(max(1,player.ratio_deaths))) 64 | percent = round(check_percent(player)) 65 | message = "Results for %s: Grief Score - %s / KDR - %s / Hit Acc. - %s" % (player.name, score, kdr, percent) 66 | add(investigate) 67 | 68 | def score_grief(connection, player, time=None): #302 = blue (0), #303 = green (1) 69 | print "start score grief" 70 | color = connection not in connection.protocol.players and connection.colors 71 | minutes = float(time or 2) 72 | if minutes < 0.0: 73 | raise ValueError() 74 | time = reactor.seconds() - minutes * 60.0 75 | blocks_removed = player.blocks_removed or [] 76 | blocks = [b[1] for b in blocks_removed if b[0] >= time] 77 | player_name = player.name 78 | team_id = player.team.id #0=blue, 1=green 79 | print "name/team set" 80 | gscore = 0 #griefscore 81 | map_blocks = 0 82 | team_blocks = 0 83 | enemy_blocks = 0 84 | team_harmed = 0 85 | enemy_harmed = 0 86 | print "init values set" 87 | if len(blocks): 88 | print "len blocks = true, blocks found" 89 | total_blocks = len(blocks) 90 | info = blocks 91 | for info in blocks: 92 | if info: 93 | name, team = info 94 | if name != player_name and team == team_id: 95 | team_blocks+= 1 96 | elif team != team_id: 97 | enemy_blocks+=1 98 | else: 99 | map_blocks+= 1 100 | print "second for done" 101 | infos = set(blocks) 102 | infos.discard(None) 103 | for name, team in infos: 104 | if name != player_name and team == team_id: 105 | team_harmed += 1 106 | elif team != team_id: 107 | enemy_harmed += 1 108 | print "third for done" 109 | else: 110 | print "len blocks = false, no blocks found" 111 | total_blocks = 0 112 | 113 | #heuristic checks start here 114 | #if they didn't break any blocks at all, they probably aren't griefing. 115 | if total_blocks == 0: 116 | print "no blocks, ending" 117 | return 0 118 | #checks on team blocks destroyed 119 | if team_blocks > 0 and team_blocks <= 5: 120 | gscore += 1 121 | elif team_blocks > 5 and team_blocks <= 10: 122 | gscore += 2 123 | elif team_blocks > 10 and team_blocks <= 25: 124 | gscore += 4 125 | elif team_blocks > 25 and team_blocks <= 50: 126 | gscore += 6 127 | elif team_blocks > 50: 128 | gscore += 10 129 | print "team blocks set" 130 | #team / total ratio checks 131 | if total_blocks != 0: 132 | ttr = (float(team_blocks) / float(total_blocks)) * 100 133 | if ttr > 5 and ttr <= 20: 134 | gscore += 1 135 | elif ttr > 20 and ttr <= 50: 136 | gscore += 2 137 | elif ttr > 50 and ttr <= 80: 138 | gscore += 3 139 | elif ttr > 80: 140 | gscore += 4 141 | print "ttr set" 142 | #teammates harmed check 143 | if team_harmed == 1: 144 | gscore += 1 145 | elif team_harmed > 2 and team_harmed <= 4: 146 | gscore += 3 147 | elif team_harmed > 4: 148 | gscore += 6 149 | print "team harmed set" 150 | print "mb: %s, tb: %s, eb: %s, Tb: %s, th: %s, ttr: %s, eh: %s, gs: %s" % (map_blocks, team_blocks, enemy_blocks, total_blocks, team_harmed, ttr, enemy_harmed, gscore) 151 | return gscore 152 | 153 | def check_percent(self): 154 | if self.weapon == RIFLE_WEAPON: 155 | if self.semi_hits == 0 or self.semi_count == 0: 156 | return 0; 157 | else: 158 | return (float(self.semi_hits)/float(self.semi_count)) * 100 159 | elif self.weapon == SMG_WEAPON: 160 | if self.smg_hits == 0 or self.smg_count == 0: 161 | return 0; 162 | else: 163 | return (float(self.smg_hits)/float(self.smg_count)) * 100 164 | elif self.weapon == SHOTGUN_WEAPON: 165 | if self.shotgun_hits == 0 or self.shotgun_count == 0: 166 | return 0; 167 | else: 168 | return float(self.shotgun_hits)/float(self.shotgun_count) 169 | 170 | def apply_script(protocol, connection, config): 171 | def send_slur_nick(connection): 172 | badmin_punish(connection, 'kick', 'Being a racist') 173 | 174 | def badmin_punish(connection, punishment='warn', reason = "Being a meany face"): 175 | connection.protocol.irc_say("* @Badmin: %s is being punished. Type: %s (Reason: %s)" % (connection.name, punishment, reason)) 176 | if punishment == "ban": 177 | connection.ban('@Badmin: ' + reason, connection.protocol.votekick_ban_duration) 178 | elif punishment == "kick": 179 | connection.kick('@Badmin: ' + reason) 180 | elif punishment == "warn": 181 | connection.protocol.send_chat(" @Badmin: Hey %s, %s" % (connection.name, reason)) 182 | 183 | class BadminConnection(connection): 184 | def on_chat(self, value, global_message): 185 | if slur_match(self, value) and LANGUAGE_FILTER_ENABLED == True: 186 | reactor.callLater(1.0, send_slur_nick, self) 187 | return connection.on_chat(self, value, global_message) 188 | class BadminProtocol(protocol): 189 | def start_votekick(self, connection, player, reason = None): 190 | if reason == None and BLANK_VOTEKICK_ENABLED == True: 191 | connection.protocol.irc_say("* @Badmin: %s is attempting a blank votekick (against %s)" % (connection.name, player.name)) 192 | return "@Badmin: You must input a reason for the votekick (/votekick name reason)" 193 | #print "before aimbot check" 194 | #print player.ratio_kills/float(max(1,player.ratio_deaths)) 195 | if aimbot_match(self, reason) and SCORE_AIMBOT_ENABLED == True: 196 | #print "made aimbot check" 197 | score = round(player.ratio_kills/float(max(1,player.ratio_deaths))) 198 | percent = round(check_percent(player)) 199 | #print "score: %s, acc: %s" % (score, percent) 200 | if score >= SCORE_AIMBOT_BAN: 201 | badmin_punish(player, "ban", "Suspected Aimbotting (Kicker: %s, KDR: %s, Hit Acc: %s)" % (connection.name, score, percent)) 202 | return 203 | if score >= SCORE_AIMBOT_KICK: 204 | badmin_punish(player, "kick", "Suspected Aimbotting (Kicker: %s, KDR: %s, Hit Acc: %s)" % (connection.name, score, percent)) 205 | return 206 | if score >= SCORE_AIMBOT_WARN: 207 | badmin_punish(player, "warn", "People think you're aimbotting! (KDR: %s, Hit Acc: %s)" % (score, percent)) 208 | return protocol.start_votekick(self, connection, player, reason) 209 | if score >= SCORE_AIMBOT_UNCERTAIN: 210 | connection.protocol.irc_say("* @Badmin: Aimbot vote: (KDR: %s, Hit Acc: %s)" % (score, percent)) 211 | return protocol.start_votekick(self, connection, player, reason) 212 | if score < SCORE_AIMBOT_UNCERTAIN: 213 | connection.protocol.irc_say("* @Badmin: I've cancelled an aimbot votekick! Kicker: %s, Kickee: %s, KDR: %s, Hit Acc: %s" % (connection.name, player.name, score, percent)) 214 | return "@Badmin: This player is not aimbotting." 215 | #print "went too far (aimbot)" 216 | if grief_match(self, reason) and SCORE_GRIEF_ENABLED == True: 217 | #print "made grief check" 218 | score = score_grief(connection, player) 219 | if score >= SCORE_GRIEF_BAN: 220 | badmin_punish(player, "ban", "Griefing (Kicker: %s, GS: %s)" % (connection.name, score)) 221 | return 222 | if score >= SCORE_GRIEF_KICK: 223 | badmin_punish(player, "kick", "Griefing (Kicker: %s, GS: %s)" % (connection.name, score)) 224 | return 225 | if score >= SCORE_GRIEF_WARN: 226 | badmin_punish(player, "warn", "Stop Griefing! (GS: %s)" % score) 227 | return protocol.start_votekick(self, connection, player, reason) 228 | if score >= SCORE_GRIEF_UNCERTAIN: 229 | connection.protocol.irc_say("* @Badmin: Grief Score: %s" % score) 230 | return protocol.start_votekick(self, connection, player, reason) 231 | if score < SCORE_GRIEF_UNCERTAIN: 232 | connection.protocol.irc_say("* @Badmin: I've cancelled a griefing votekick! Kicker: %s, Kickee: %s, Score: %s" % (connection.name, player.name, score)) 233 | return "@Badmin: This player has not been griefing." 234 | return protocol.start_votekick(self, connection, player, reason) 235 | 236 | return BadminProtocol, BadminConnection -------------------------------------------------------------------------------- /box.py: -------------------------------------------------------------------------------- 1 | from commands import add, admin 2 | import buildbox 3 | import cbc 4 | 5 | # requires buildbox.py script in the /scripts folder 6 | 7 | @admin 8 | def box(connection, filled = ""): 9 | if connection.boxing > 0: 10 | connection.boxing = 0 11 | return 'Building generator cancelled' 12 | else: 13 | connection.boxing = 1 14 | connection.boxing_filled = filled.lower() == "filled" 15 | return 'Place first corner block' 16 | add(box) 17 | 18 | def apply_script(protocol, connection, config): 19 | protocol, connection = cbc.apply_script(protocol, connection, config) 20 | 21 | class BoxMakerConnection(connection): 22 | def __init__(self, *arg, **kw): 23 | connection.__init__(self, *arg, **kw) 24 | self.boxing = 0 25 | self.boxing_filled = 0 26 | self.box_x = 0 27 | self.box_y = 0 28 | self.box_z = 0 29 | 30 | def build_box_filled(self, x1, y1, z1, x2, y2, z2, color = None): 31 | buildbox.build_filled(self.protocol, x1, y1, z1, x2, y2, z2, color or self.color, self.god, self.god_build) 32 | 33 | def build_box(self, x1, y1, z1, x2, y2, z2, color = None): 34 | buildbox.build_empty(self.protocol, x1, y1, z1, x2, y2, z2, color or self.color, self.god, self.god_build) 35 | 36 | def on_block_build(self, x, y, z): 37 | if self.boxing == 2: 38 | self.boxing = 0 39 | if self.boxing_filled == 0: 40 | self.build_box(self.box_x, self.box_y, self.box_z, x, y, z) 41 | else: 42 | self.build_box_filled(self.box_x, self.box_y, self.box_z, x, y, z) 43 | self.send_chat('Box created!') 44 | if self.boxing == 1: 45 | self.box_x = x 46 | self.box_y = y 47 | self.box_z = z 48 | self.send_chat('Now place opposite corner block') 49 | self.boxing = 2 50 | return connection.on_block_build(self, x, y, z) 51 | 52 | class BoxMakerProtocol(protocol): 53 | def on_map_change(self, map): 54 | for connection in self.clients: 55 | connection.boxing = 0 56 | protocol.on_map_change(self, map) 57 | 58 | return BoxMakerProtocol, BoxMakerConnection -------------------------------------------------------------------------------- /buildbox.py: -------------------------------------------------------------------------------- 1 | from pyspades.contained import BlockLine, SetColor 2 | from pyspades.common import make_color 3 | from pyspades.constants import * 4 | from itertools import product 5 | import cbc 6 | 7 | # this file must be in the /scripts folder, but it is NOT included in config.txt 8 | 9 | MAX_LINE_BLOCKS = 64 10 | 11 | def ordered_product(ranges, order): 12 | """Iterates through ranges in the order specified in order, but each yeild returns in the original order of the ranges""" 13 | 14 | order_inv = zip(*sorted(zip(order, sorted(order))))[1] 15 | 16 | for prod in product(*(ranges[o] for o in order)): 17 | yield tuple(prod[o] for o in order_inv) 18 | 19 | def build_filled_generator(protocol, x1, y1, z1, x2, y2, z2, color, god = False, god_build = False): 20 | # create a player instance, freed when the generator is done 21 | # other scripts that also use ServerPlayer won't get the same id! 22 | # this won't be necessary in 1.0 23 | splayer = cbc.ServerPlayer() 24 | 25 | line = BlockLine() 26 | line.player_id = splayer.player_id 27 | 28 | set_color = SetColor() 29 | set_color.value = make_color(*color) 30 | set_color.player_id = splayer.player_id 31 | protocol.send_contained(set_color, save = True) 32 | packets = 1 33 | 34 | check_protected = hasattr(protocol, 'protected') 35 | if god_build and protocol.god_blocks is None: 36 | protocol.god_blocks = set() 37 | 38 | map = protocol.map 39 | 40 | ranges = [xrange(min(x1 , x2) , max(x1 , x2)+1) 41 | , xrange(min(y1 , y2) , max(y1 , y2)+1) 42 | , xrange(min(z1 , z2) , max(z1 , z2)+1)] 43 | 44 | order = zip(*sorted(zip([len(x) for x in ranges], [0, 1, 2])))[1] 45 | 46 | # set the first block position 47 | prod = ordered_product(ranges, order) 48 | line.x1, line.y1, line.z1 = prod.next() 49 | line.x2 = line.x1 50 | line.y2 = line.y1 51 | line.z2 = line.z1 52 | map.set_point(line.x1, line.y1, line.z1, color) 53 | 54 | for x, y, z in prod: 55 | packets = 0 56 | if not god and check_protected and protocol.is_protected(x, y, z): 57 | continue 58 | if god_build: 59 | protocol.god_blocks.add((x, y, z)) 60 | changed = (line.x1 != x or line.x2 != x) + (line.y1 != y or line.y2 != y) + (line.z1 != z or line.z2 != z) 61 | dist = abs(line.x1 - x) + abs(line.y1 - y) + abs(line.z1 - z) 62 | if changed > 1 or dist >= MAX_LINE_BLOCKS: 63 | protocol.send_contained(line, save = True) 64 | packets += 2 65 | line.x1 = x 66 | line.y1 = y 67 | line.z1 = z 68 | line.x2 = x 69 | line.y2 = y 70 | line.z2 = z 71 | map.set_point(x, y, z, color) 72 | 73 | yield packets, 0 74 | protocol.send_contained(line, save = True) 75 | yield 1, 0 76 | 77 | def build_filled(protocol, x1, y1, z1, x2, y2, z2, color, god = False, god_build = False): 78 | if (x1 < 0 or x1 >= 512 or y1 < 0 or y1 >= 512 or z1 < 0 or z1 > 64 or 79 | x2 < 0 or x2 >= 512 or y2 < 0 or y2 >= 512 or z2 < 0 or z2 > 64): 80 | raise ValueError("Invalid coordinates: (%i, %i, %i):(%i, %i, %i)" % (x1, y1, z1, x2, y2, z2)) 81 | protocol.cbc_add(build_filled_generator(protocol, x1, y1, z1, x2, y2, z2, color)) 82 | 83 | def build_empty(protocol, x1, y1, z1, x2, y2, z2, color, god = False, god_build = False): 84 | build_filled(protocol, x1, y1, z1, x1, y2, z2, color, god, god_build) 85 | build_filled(protocol, x2, y1, z1, x2, y2, z2, color, god, god_build) 86 | build_filled(protocol, x1, y1, z1, x2, y1, z2, color, god, god_build) 87 | build_filled(protocol, x1, y2, z1, x2, y2, z2, color, god, god_build) 88 | build_filled(protocol, x1, y1, z1, x2, y2, z1, color, god, god_build) 89 | build_filled(protocol, x1, y1, z2, x2, y2, z2, color, god, god_build) 90 | -------------------------------------------------------------------------------- /cbc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script tool for progressively applying a large number of block changes to the map. 3 | 4 | Usage: 5 | # At the top of the file 6 | import cbc 7 | 8 | # in your apply_script() function 9 | 10 | apply_script(protocol, connection, config) 11 | protocol, connection = cbc.apply_script(protocol, connection, config) 12 | 13 | # start 14 | generator = self.create_generator_function() 15 | 16 | handle = self.protocol.cbc_add(generator) 17 | # or 18 | handle = self.protocol.cbc_add(generator, update_interval, self.callback_function, *callback_args) 19 | 20 | # update_interval is the time (in seconds) between calls to `self.callback_function` 21 | 22 | # stop 23 | self.protocol.cbc_cancel(handle) 24 | 25 | Callback receives these args: 26 | 27 | def callback_function(self, cbc_type, progress, total_elapsed_seconds, *callback_args): 28 | 29 | The generator function should `yield , ` for each unique packet sent to clients 30 | Where packets is the number of packets sent this iteration, and progress is the current progress percentage 31 | 32 | Author: infogulch 33 | """ 34 | 35 | from twisted.internet.task import LoopingCall 36 | import time 37 | import random 38 | 39 | class _CbcInfo: 40 | generator = None 41 | update_interval = 0.0 42 | callback = None 43 | callback_args = None 44 | last_update = time.time() 45 | start = time.time() 46 | progress = 0.0 47 | 48 | def __init__(self, generator, update_interval, callback, callback_args): 49 | self.generator = generator 50 | self.update_interval = update_interval 51 | self.callback = callback 52 | self.callback_args = callback_args 53 | 54 | #note: client crashes when this goes over ~50 55 | class ServerPlayer(object): 56 | server_players = set() 57 | 58 | def __init__(self): 59 | id = 33 60 | while id in ServerPlayer.server_players: 61 | id += 1 62 | self.player_id = id 63 | ServerPlayer.server_players.add(id) 64 | 65 | def __del__(self): 66 | ServerPlayer.server_players.discard(self.player_id) 67 | 68 | TIME_BETWEEN_CYCLES = 0.06 69 | MAX_UNIQUE_PACKETS = 30 # per 'cycle', each block op is at least 1 70 | MAX_PACKETS = 300 # per 'cycle' cap for (unique packets * players) 71 | MAX_TIME = 0.03 # max time each cycle takes 72 | 73 | def apply_script(protocol, connection, config): 74 | if hasattr(protocol, 'cbc_add'): 75 | return protocol, connection 76 | 77 | class CycleBlockCoiteratorProtocol(protocol): 78 | CBC_UPDATE, CBC_CANCELLED, CBC_FINISHED = range(3) 79 | 80 | def __init__(self, *args, **kwargs): 81 | protocol.__init__(self, *args, **kwargs) 82 | 83 | self._cbc_running = False 84 | self._cbc_generators = {} 85 | self._cbc_call = LoopingCall(self._cbc_cycle) 86 | 87 | def cbc_add(self, generator, update_time = 10.0, callback = None, *args): 88 | info = _CbcInfo(generator, update_time, callback, args) 89 | handle = max(self._cbc_generators.keys() + [0]) + 1 90 | self._cbc_generators[handle] = info 91 | if not self._cbc_running: 92 | self._cbc_running = True 93 | self._cbc_call.start(TIME_BETWEEN_CYCLES, False) 94 | #this handle lets you cancel in the middle later 95 | return handle 96 | 97 | def cbc_cancel(self, handle): 98 | if self._cbc_generators.has_key(handle): 99 | info = self._cbc_generators[handle] 100 | if info.callback is not None: 101 | info.callback(CANCELLED, info.progress, time.time() - info.start, *info.callback_args) 102 | del self._cbc_generators[handle] 103 | 104 | def _cbc_cycle(self): 105 | sent_unique = sent_total = progress = 0 106 | current_handle = None 107 | cycle_time = time.time() 108 | while self._cbc_generators: 109 | try: 110 | for handle, info in self._cbc_generators.iteritems(): 111 | if sent_unique > MAX_UNIQUE_PACKETS: 112 | return 113 | if sent_total > MAX_PACKETS: 114 | return 115 | if time.time() - cycle_time > MAX_TIME: 116 | return 117 | current_handle = handle 118 | sent, progress = info.generator.next() 119 | sent_unique += sent 120 | sent_total += sent * len(self.players) 121 | if (time.time() - info.last_update > info.update_interval): 122 | info.last_update = time.time() 123 | info.progress = progress 124 | if not info.callback is None: 125 | info.callback(self.CBC_UPDATE, progress, time.time() - info.start, *info.callback_args) 126 | except StopIteration: 127 | info = self._cbc_generators[current_handle] 128 | if info.callback is not None: 129 | info.callback(self.CBC_FINISHED, progress, time.time() - info.start, *info.callback_args) 130 | del self._cbc_generators[current_handle] 131 | else: 132 | self._cbc_call.stop() 133 | self._cbc_running = False 134 | 135 | def on_map_change(self, map): 136 | if hasattr(self, '_cbc_generators'): 137 | for handle in self._cbc_generators.keys(): 138 | self.cbc_cancel(handle) 139 | protocol.on_map_change(self, map) 140 | 141 | def on_map_leave(self): 142 | if hasattr(self, '_cbc_generators'): 143 | for handle in self._cbc_generators.keys(): 144 | self.cbc_cancel(handle) 145 | protocol.on_map_leave(self) 146 | 147 | return CycleBlockCoiteratorProtocol, connection -------------------------------------------------------------------------------- /clearbox.py: -------------------------------------------------------------------------------- 1 | from pyspades.contained import BlockAction 2 | from pyspades.constants import * 3 | from itertools import product, chain 4 | import cbc 5 | 6 | # this file must be in the /scripts folder, but it is NOT included in config.txt 7 | 8 | def clear_solid_generator(protocol, x1, y1, z1, x2, y2, z2, god = False, destroy = True): 9 | block_action = BlockAction() 10 | block_action.value = DESTROY_BLOCK 11 | splayer = cbc.ServerPlayer() 12 | block_action.player_id = splayer.player_id 13 | map = protocol.map 14 | check_protected = hasattr(protocol, 'protected') 15 | x1, x2 = sorted((x1 , x2)) 16 | y1, y2 = sorted((y1 , y2)) 17 | z1, z2 = sorted((z1 , z2)) 18 | clear = map.destroy_point if destroy else map.remove_point 19 | get_solid = map.get_solid 20 | for x, y, z in product( xrange(x1, x2+1) 21 | , xrange(y1, y2+1) 22 | , xrange(z1, z2+1)): 23 | packets = 0 24 | if get_solid(x, y, z) and (god or 25 | not (check_protected and protocol.is_protected(x, y, z)) #not protected 26 | and not (protocol.god_blocks is not None and (x, y, z) in protocol.god_blocks)): #not a god block 27 | block_action.x = x 28 | block_action.y = y 29 | block_action.z = z 30 | protocol.send_contained(block_action, save = True) 31 | clear(x, y, z) 32 | packets = 1 33 | yield packets, 0 34 | 35 | def clear_solid(protocol, x1, y1, z1, x2, y2, z2, god = False): 36 | if (x1 < 0 or x1 >= 512 or y1 < 0 or y1 >= 512 or z1 < 0 or z1 > 64 or 37 | x2 < 0 or x2 >= 512 or y2 < 0 or y2 >= 512 or z2 < 0 or z2 > 64): 38 | raise ValueError('Invalid coordinates: (%i, %i, %i):(%i, %i, %i)' % (x1, y1, z1, x2, y2, z2)) 39 | protocol.cbc_add(clear_solid_generator(protocol, x1, y1, z1, x2, y2, z2, god)) 40 | 41 | def clear(protocol, x1, y1, z1, x2, y2, z2, god = False): 42 | x1, x2 = sorted((x1, x2)) 43 | y1, y2 = sorted((y1, y2)) 44 | z1, z2 = sorted((z1, z2)) 45 | lst = ( 46 | clear_solid_generator(protocol, x1, y1, z2, x2, y2, z2, god, False) 47 | , clear_solid_generator(protocol, x1, y1, z1, x1, y2, z2, god, False) 48 | , clear_solid_generator(protocol, x2, y1, z1, x2, y2, z2, god, False) 49 | , clear_solid_generator(protocol, x1, y1, z1, x2, y1, z2, god, False) 50 | , clear_solid_generator(protocol, x1, y2, z1, x2, y2, z2, god, False) 51 | , clear_solid_generator(protocol, x1, y1, z1, x2, y2, z1, god, False) 52 | , clear_solid_generator(protocol, x1, y1, z1, x2, y2, z2, god, True)) 53 | 54 | protocol.cbc_add(chain.from_iterable(lst)) 55 | -------------------------------------------------------------------------------- /db.py: -------------------------------------------------------------------------------- 1 | from commands import add, admin 2 | import clearbox 3 | import cbc 4 | 5 | # requires clearbox.py in the /scripts directory 6 | 7 | @admin 8 | def db(connection): 9 | if connection.deboxing > 0: 10 | connection.deboxing = 0 11 | return 'DeBox cancelled' 12 | else: 13 | connection.deboxing = 1 14 | return 'Break first corner block' 15 | add(db) 16 | 17 | def apply_script(protocol, connection, config): 18 | protocol, connection = cbc.apply_script(protocol, connection, config) 19 | 20 | class ClearBoxMakerConnection(connection): 21 | def __init__(self, *arg, **kw): 22 | connection.__init__(self, *arg, **kw) 23 | self.deboxing = 0 24 | self.clearbox_x = 0 25 | self.clearbox_y = 0 26 | self.clearbox_z = 0 27 | 28 | def clear_box_solid(self, x1, y1, z1, x2, y2, z2): 29 | clearbox.clear_solid(self.protocol, x1, y1, z1, x2, y2, z2, self.god) 30 | 31 | def clear_box(self, x1, y1, z1, x2, y2, z2): 32 | clearbox.clear(self.protocol, x1, y1, z1, x2, y2, z2, self.god) 33 | 34 | def on_block_removed(self, x, y, z): 35 | if self.deboxing == 2: 36 | self.deboxing = 0 37 | self.clear_box(self.clearbox_x, self.clearbox_y, self.clearbox_z, x, y, z) 38 | self.send_chat('Destroying box!') 39 | if self.deboxing == 1: 40 | self.clearbox_x = x 41 | self.clearbox_y = y 42 | self.clearbox_z = z 43 | self.send_chat('Now break opposite corner block') 44 | self.deboxing = 2 45 | return connection.on_block_removed(self, x, y, z) 46 | 47 | class ClearBoxMakerProtocol(protocol): 48 | def on_map_change(self, map): 49 | for connection in self.clients: 50 | connection.deboxing = 0 51 | protocol.on_map_change(self, map) 52 | 53 | return ClearBoxMakerProtocol, ClearBoxMakerConnection -------------------------------------------------------------------------------- /df.py: -------------------------------------------------------------------------------- 1 | from commands import add, admin 2 | import clearbox 3 | import cbc 4 | 5 | # requires clearbox.py in the /scripts directory 6 | 7 | @admin 8 | def df(connection): 9 | if connection.deflooring > 0: 10 | connection.deflooring = 0 11 | return 'DeFloor cancelled' 12 | else: 13 | connection.deflooring = 1 14 | return 'Break first corner block' 15 | add(df) 16 | 17 | def apply_script(protocol, connection, config): 18 | protocol, connection = cbc.apply_script(protocol, connection, config) 19 | 20 | class ClearFloorMakerConnection(connection): 21 | def __init__(self, *args, **kwargs): 22 | connection.__init__(self, *args, **kwargs) 23 | self.deflooring = 0 24 | self.clearfloor_x = 0 25 | self.clearfloor_y = 0 26 | self.clearfloor_z = 0 27 | 28 | def on_block_removed(self, x, y, z): 29 | if self.deflooring == 2: 30 | self.deflooring = 0 31 | if self.clearfloor_z != z: 32 | self.send_chat('Surface is uneven! Using first height.') 33 | clearbox.clear_solid(self.protocol, self.clearfloor_x, self.clearfloor_y, self.clearfloor_z, x, y, self.clearfloor_z, self.god) 34 | self.send_chat('Floor destroyed!') 35 | if self.deflooring == 1: 36 | self.clearfloor_x = x 37 | self.clearfloor_y = y 38 | self.clearfloor_z = z 39 | self.send_chat('Now break opposite corner block') 40 | self.deflooring = 2 41 | return connection.on_block_removed(self, x, y, z) 42 | 43 | class ClearFloorMakerProtocol(protocol): 44 | def on_map_change(self, map): 45 | for connection in self.clients: 46 | connection.deflooring = 0 47 | protocol.on_map_change(self, map) 48 | 49 | return ClearFloorMakerProtocol, ClearFloorMakerConnection 50 | -------------------------------------------------------------------------------- /dirtnade.py: -------------------------------------------------------------------------------- 1 | """ 2 | Makes grenades create blocks. 3 | 4 | Maintainer: hompy 5 | """ 6 | 7 | from pyspades.server import block_action 8 | from pyspades.constants import * 9 | import cbc 10 | 11 | DIRTNADE_BLOCKS = 19 12 | 13 | def apply_script(protocol, connection, config): 14 | protocol, connection = cbc.apply_script(protocol, connection, config) 15 | 16 | def try_add_node(map, x, y, z, list): 17 | if x < 0 or x >= 512 or y < 0 or y >= 512 or z < 0 or z >= 62: 18 | return 19 | if map.get_solid(x, y, z): 20 | return 21 | list.append((x, y, z)) 22 | 23 | class DirtGrenadeConnection(connection): 24 | def dirtnade_generator(self, x, y, z, blocks): 25 | map = self.protocol.map 26 | list = [] 27 | try_add_node(map, x, y, z, list) 28 | block_action.value = BUILD_BLOCK 29 | block_action.player_id = self.player_id 30 | while list: 31 | x, y, z = list.pop(0) 32 | if connection.on_block_build_attempt(self, x, y, z) == False: 33 | continue 34 | block_action.x = x 35 | block_action.y = y 36 | block_action.z = z 37 | self.protocol.send_contained(block_action, save = True) 38 | map.set_point(x, y, z, self.color) 39 | yield 1, 0 40 | blocks -= 1 41 | if blocks == 0: 42 | break 43 | try_add_node(map, x, y, z - 1, list) 44 | try_add_node(map, x, y - 1, z, list) 45 | try_add_node(map, x, y + 1, z, list) 46 | try_add_node(map, x - 1, y, z, list) 47 | try_add_node(map, x + 1, y, z, list) 48 | try_add_node(map, x, y, z + 1, list) 49 | self.protocol.update_entities() 50 | 51 | def grenade_exploded(self, grenade): 52 | if self.name is None: 53 | return 54 | if self.weapon != 1: 55 | return connection.grenade_exploded(self, grenade) 56 | position = grenade.position 57 | x = int(position.x) 58 | y = int(position.y) 59 | z = int(position.z) 60 | self.protocol.cbc_add(self.dirtnade_generator(x, y, z, DIRTNADE_BLOCKS)) 61 | 62 | return protocol, DirtGrenadeConnection -------------------------------------------------------------------------------- /dw.py: -------------------------------------------------------------------------------- 1 | from commands import add, admin 2 | import clearbox 3 | import cbc 4 | 5 | # requires clearbox.py in the /scripts directory 6 | 7 | def sign(x): 8 | return (x > 0) - (x < 0) 9 | 10 | @admin 11 | def dw(connection, value = ''): 12 | try: 13 | value = int(value) 14 | except ValueError: 15 | value = 0 16 | if value < 65 and value > -65 and abs(value) > 1: 17 | connection.dewalling = value 18 | return 'DeWalling %s block high wall. "/dw" to cancel.' % connection.dewalling 19 | else: 20 | connection.dewalling = None 21 | return 'No longer DeWalling. Activate with `/dw 64` to `/dw -64`' 22 | add(dw) 23 | 24 | def apply_script(protocol, connection, config): 25 | protocol, connection = cbc.apply_script(protocol, connection, config) 26 | 27 | class DeWallMakerConnection(connection): 28 | def __init__(self, *args, **kwargs): 29 | connection.__init__(self, *args, **kwargs) 30 | self.dewalling = None 31 | 32 | def on_block_removed(self, x, y, z): 33 | if self.dewalling is not None: 34 | z2 = min(61, max(0, z - self.dewalling + sign(self.dewalling))) 35 | clearbox.clear_solid(self.protocol, x, y, z, x, y, z2, self.god) 36 | return connection.on_block_removed(self, x, y, z) 37 | 38 | return protocol, DeWallMakerConnection 39 | -------------------------------------------------------------------------------- /dynfog.py: -------------------------------------------------------------------------------- 1 | import commands 2 | 3 | def apply_script(protocol, connection, config): 4 | class FogProtocol(protocol): 5 | default_fog = (128, 232, 255) 6 | def on_map_change(self, name): 7 | self.set_fog_color(getattr(self.map_info.info, 'fog', self.default_fog)) 8 | protocol.on_map_change(self, name) 9 | return FogProtocol, connection -------------------------------------------------------------------------------- /floor.py: -------------------------------------------------------------------------------- 1 | from commands import add, admin 2 | import buildbox 3 | import cbc 4 | 5 | # requires buildbox.py script in the /scripts folder 6 | 7 | @admin 8 | def floor(connection): 9 | if connection.flooring > 0: 10 | connection.flooring = 0 11 | return 'Floor generator cancelled' 12 | else: 13 | connection.flooring = 1 14 | return 'Place first corner block' 15 | add(floor) 16 | 17 | def apply_script(protocol, connection, config): 18 | protocol, connection = cbc.apply_script(protocol, connection, config) 19 | 20 | class FloorMakerConnection(connection): 21 | def __init__(self, *arg, **kw): 22 | connection.__init__(self, *arg, **kw) 23 | self.flooring = 0 24 | self.floor_x = 0 25 | self.floor_y = 0 26 | self.floor_z = 0 27 | 28 | def on_block_build(self, x, y, z): 29 | if self.flooring == 2: 30 | self.flooring = 0 31 | if self.floor_z != z: 32 | self.send_chat('Surface is uneven! Using first height.') 33 | buildbox.build_filled(self.protocol 34 | , self.floor_x, self.floor_y, self.floor_z 35 | , x, y, self.floor_z 36 | , self.color, self.god, self.god_build) 37 | if self.flooring == 1: 38 | self.floor_x = x 39 | self.floor_y = y 40 | self.floor_z = z 41 | self.send_chat('Now place opposite corner block') 42 | self.flooring = 2 43 | return connection.on_block_build(self, x, y, z) 44 | 45 | class FloorMakerProtocol(protocol): 46 | def on_map_change(self, map): 47 | for connection in self.clients: 48 | connection.flooring = 0 49 | protocol.on_map_change(self, map) 50 | 51 | return FloorMakerProtocol, FloorMakerConnection 52 | -------------------------------------------------------------------------------- /freeforall.py: -------------------------------------------------------------------------------- 1 | # Free for all script written by Yourself 2 | 3 | from random import randint 4 | 5 | # If ALWAYS_ENABLED is False, free for all can still be enabled in the map 6 | # metadata by setting the key 'free_for_all' to True in the extensions dictionary 7 | ALWAYS_ENABLED = True 8 | 9 | # If WATER_SPANS is True, then players can spawn in water 10 | WATER_SPAWNS = False 11 | 12 | HIDE_POS = (0, 0, 63) 13 | 14 | def apply_script(protocol, connection, config): 15 | class FreeForAllProtocol(protocol): 16 | free_for_all = False 17 | old_friendly_fire = None 18 | def on_map_change(self, map): 19 | extensions = self.map_info.extensions 20 | if ALWAYS_ENABLED: 21 | self.free_for_all = True 22 | else: 23 | if extensions.has_key('free_for_all'): 24 | self.free_for_all = extensions['free_for_all'] 25 | else: 26 | self.free_for_all = False 27 | if self.free_for_all: 28 | self.old_friendly_fire = self.friendly_fire 29 | self.friendly_fire = True 30 | else: 31 | if self.old_friendly_fire is not None: 32 | self.friendly_fire = self.old_friendly_fire 33 | self.old_friendly_fire = None 34 | return protocol.on_map_change(self, map) 35 | 36 | def on_base_spawn(self, x, y, z, base, entity_id): 37 | if self.free_for_all: 38 | return HIDE_POS 39 | return protocol.on_base_spawn(self, x, y, z, base, entity_id) 40 | 41 | def on_flag_spawn(self, x, y, z, flag, entity_id): 42 | if self.free_for_all: 43 | return HIDE_POS 44 | return protocol.on_flag_spawn(self, x, y, z, flag, entity_id) 45 | 46 | class FreeForAllConnection(connection): 47 | score_hack = False 48 | def on_spawn_location(self, pos): 49 | if not self.score_hack and self.protocol.free_for_all: 50 | while True: 51 | x = randint(0, 511) 52 | y = randint(0, 511) 53 | z = self.protocol.map.get_z(x, y) 54 | if z != 63 or WATER_SPAWNS: 55 | break 56 | # Magic numbers taken from server.py spawn function 57 | z -= 2.4 58 | x += 0.5 59 | y += 0.5 60 | return (x, y, z) 61 | return connection.on_spawn_location(self, pos) 62 | 63 | def on_refill(self): 64 | if self.protocol.free_for_all: 65 | return False 66 | return connection.on_refill(self) 67 | 68 | def on_flag_take(self): 69 | if self.protocol.free_for_all: 70 | return False 71 | return connection.on_flag_take(self) 72 | 73 | def on_kill(self, by, type, grenade): 74 | # Switch teams to add score hack 75 | if by is not None and by.team is self.team and self is not by: 76 | self.score_hack = True 77 | pos = self.world_object.position 78 | self.set_team(self.team.other) 79 | self.spawn((pos.x, pos.y, pos.z)) 80 | self.score_hack = False 81 | return connection.on_kill(self, by, type, grenade) 82 | 83 | return FreeForAllProtocol, FreeForAllConnection -------------------------------------------------------------------------------- /gradient.py: -------------------------------------------------------------------------------- 1 | """ 2 | Make gradient lines! 3 | 4 | /gradient r g b r g b 5 | 6 | alias: /gr 7 | 8 | Author: infogulch 9 | """ 10 | 11 | from pyspades.common import make_color 12 | from pyspades.contained import BlockAction, SetColor 13 | from pyspades.constants import * 14 | from commands import add, alias 15 | 16 | import cbc 17 | 18 | @alias('gr') 19 | def gradient(connection, *colors): 20 | if len(colors) != 6: 21 | if not connection.gradient_colors: 22 | return 'Usage: /gradient r g b r g b, OR choose from & to colors with /grf /grt' 23 | if not connection.gradient_enabled: 24 | connection.gradient_enabled = True 25 | return 'Gradient enabled. Colors are: (%i, %i, %i) (%i, %i, %i)' % ( 26 | connection.gradient_colors[0] + connection.gradient_colors[1]) 27 | else: 28 | connection.gradient_enabled = False 29 | return 'No longer making gradients.' 30 | try: 31 | colors = tuple(int(c) for c in colors) 32 | connection.gradient_colors = colors[:3], colors[3:] 33 | connection.gradient_enabled = True 34 | return 'The next line you build will create a gradient from (%i,%i,%i) to (%i,%i,%i).' % colors 35 | except ValueError: 36 | return 'All args must be integers.' 37 | 38 | @alias('grf') 39 | def gradientfrom(connection): 40 | if not connection.gradient_colors: 41 | connection.gradient_colors = [(0,0,0), (0,0,0)] 42 | connection.gradient_colors[0] = connection.color 43 | return 'Gradient from color is now: (%i %i %i)' % connection.color 44 | 45 | @alias('grt') 46 | def gradientto(connection): 47 | if not connection.gradient_colors: 48 | connection.gradient_colors = [(0,0,0), (0,0,0)] 49 | connection.gradient_colors[1] = connection.color 50 | return 'Gradient to color is now: (%i %i %i)' % connection.color 51 | 52 | add(gradient) 53 | add(gradientfrom) 54 | add(gradientto) 55 | 56 | def build_gradient_line(protocol, colors, points): 57 | sp = cbc.ServerPlayer() 58 | 59 | block_action = BlockAction() 60 | block_action.player_id = sp.player_id 61 | block_action.value = BUILD_BLOCK 62 | 63 | set_color = SetColor() 64 | set_color.player_id = sp.player_id 65 | 66 | color_range = zip(*colors) 67 | 68 | lp = len(points) - 1 69 | map = protocol.map 70 | for i in xrange(len(points)): 71 | if lp: 72 | pct = 1 - (i+0.0) / lp, (i+0.0) / lp 73 | else: 74 | pct = (1,0) 75 | 76 | color = tuple(int(round(sum(c*p for c,p in zip(crng, pct)))) for crng in color_range) 77 | 78 | map.set_point(*points[i], color = color) 79 | 80 | set_color.value = make_color(*color) 81 | protocol.send_contained(set_color, save = True) 82 | 83 | block_action.x, block_action.y, block_action.z = points[i] 84 | protocol.send_contained(block_action, save = True) 85 | 86 | def apply_script(protocol, connection, config): 87 | class GradientConnection(connection): 88 | def __init__(self, *args, **kwargs): 89 | connection.__init__(self, *args, **kwargs) 90 | self.gradient_colors = [] 91 | self.gradient_enabled = False 92 | 93 | def on_line_build_attempt(self, points): 94 | if connection.on_line_build_attempt(self, points) == False: 95 | return False 96 | if self.gradient_enabled: 97 | build_gradient_line(self.protocol, self.gradient_colors, points) 98 | return False 99 | 100 | return protocol, GradientConnection -------------------------------------------------------------------------------- /hp.py: -------------------------------------------------------------------------------- 1 | """ Get a player's hp! 2 | 3 | /hp 4 | 5 | Author: infogulch 6 | """ 7 | 8 | from commands import get_player, add, InvalidPlayer, InvalidSpectator 9 | 10 | def hp(connection, player_name): 11 | try: 12 | player = get_player(connection.protocol, player_name, False) 13 | except InvalidPlayer: 14 | return 'Invalid player' 15 | except InvalidSpectator: 16 | return 'Player is a spectator' 17 | 18 | return "%s's HP is: %i" % (player.name, player.hp) 19 | 20 | add(hp) 21 | 22 | def apply_script(protocol, connection, config): 23 | return protocol, connection -------------------------------------------------------------------------------- /jail.py: -------------------------------------------------------------------------------- 1 | """ 2 | Jail script by PXYC 3 | 4 | /jail [reason] jails a player. Default reason is "None" 5 | /free frees a player. 6 | /jailed [player] checks if a player is jailed. If no arguments are entered, it lists jailed players. 7 | /jailbreak frees all jailed players. 8 | """ 9 | 10 | from pyspades.common import to_coordinates, coordinates 11 | from commands import add, admin, get_player, join_arguments, name, alias 12 | from pyspades.constants import * 13 | 14 | jail_location = 0, 0, 0 # x, y, z of the jail 15 | jail_coords = [ ] # e.g. ["B4", "B5"] 16 | 17 | jail_list = [] 18 | 19 | @name('jail') 20 | @alias('j') 21 | @admin 22 | def jail_player(connection, value = None, *args): 23 | protocol = connection.protocol # Meh 24 | player = get_player(protocol, value) # Get player 25 | reason = join_arguments(args[0:]) # Convert reason args into one string 26 | if player not in protocol.players: 27 | raise ValueError() # If player doesn't exist, raise error 28 | else: 29 | if player.jailed: 30 | return 'Player ' + player.name + ' is already jailed!' # Player is already jailed! 31 | elif not player.jailed: 32 | player.jailed = True # Set player to jailed 33 | player.reason = reason 34 | player.squad = None 35 | player.squad_pref = None 36 | player.set_location(jail_location) # Move player to jail 37 | connection.protocol.send_chat("%s was sent to jail by %s for reason(s): %s" % (player.name, connection.name, reason)) # Message 38 | connection.protocol.irc_say("* %s jailed %s for reason: %s" % (connection.name, player.name, reason)) # Message 39 | jail_list.append(player.name) 40 | add(jail_player) # Add command 41 | 42 | @name('jailed') 43 | def is_jailed(connection, value = None): 44 | if value is None: 45 | if not jail_list: 46 | return 'No jailed players.' 47 | else: 48 | return "Jailed players: " + ", ".join(jail_list) 49 | elif value is not None: 50 | protocol = connection.protocol 51 | player = get_player(protocol, value) 52 | if player not in protocol.players: 53 | raise ValueError() 54 | else: 55 | if player.jailed: 56 | return 'Player %s jailed for: %s' % (player.name, player.reason) 57 | else: 58 | return 'Player %s is not jailed.' % (player.name) 59 | add(is_jailed) 60 | 61 | @name('free') 62 | @admin 63 | def free_from_jail(connection, value): 64 | protocol = connection.protocol # Meh 65 | player = get_player(protocol, value) # Get player 66 | if player not in protocol.players: 67 | raise ValueError() # Errors again 68 | else: 69 | if not player.jailed: # If player isn't jailed 70 | return 'Player ' + player.name + ' is not jailed!' # Message 71 | elif player.jailed: # If player is jailed 72 | player.jailed = False # Player is not jailed anymore 73 | player.kill() # Kill the player 74 | connection.protocol.send_chat("%s was freed from jail by %s" % (player.name, connection.name)) # Message 75 | connection.protocol.irc_say('* %s was freed from jail by %s' % (player.name, connection.name)) # Message 76 | jail_list.remove(player.name) 77 | 78 | add(free_from_jail) 79 | 80 | @name('jailbreak') 81 | @admin 82 | def free_all(connection): 83 | protocol = connection.protocol 84 | for playersJailed in jail_list: 85 | player = get_player(protocol, playersJailed) 86 | player.kill() 87 | player.jailed = False 88 | player.reason = None 89 | jail_list.remove(playersJailed) 90 | return 'All players freed.' 91 | 92 | add(free_all) 93 | 94 | def apply_script(protocol, connection, config): 95 | class JailConnection(connection): 96 | jailed = False 97 | def on_spawn_location(self, pos): 98 | if self.jailed: 99 | return jail_location 100 | return connection.on_spawn_location(self, pos) 101 | def on_block_build_attempt(self, x, y, z): 102 | x, y, z = self.get_location() 103 | coord = to_coordinates(x, y) 104 | if self.jailed: 105 | self.send_chat("You can't build when you're jailed! You were jailed for %s" % (self.reason)) 106 | return False 107 | elif coord in jail_coords and not self.user_types.admin: # Stuff 108 | self.send_chat("You can't build near the jail, %s!" % self.name) 109 | return False 110 | return connection.on_block_build_attempt(self, x, y, z) 111 | def on_block_destroy(self, x, y, z, mode): 112 | x, y, z = self.get_location() 113 | coord = to_coordinates(x, y) 114 | if self.jailed: 115 | self.send_chat("You can't destroy blocks when you're in jail! You were jailed for: %s" % (self.reason)) 116 | return False 117 | elif coord in jail_coords and not self.user_types.admin: 118 | self.send_chat("Stop trying to destroy the jail, %s!" % self.name) 119 | return False 120 | return connection.on_block_destroy(self, x, y, z, mode) 121 | def on_line_build_attempt(self, points): 122 | x, y, z = self.get_location() 123 | coord = to_coordinates(x, y) 124 | if self.jailed: 125 | self.send_chat("You can't build when you're jailed! You were jailed for: %s" % (self.reason)) 126 | return False 127 | elif coord in jail_coords and not self.user_types.admin: 128 | self.send_chat("You can't build near the jail, %s!" % self.name) 129 | return False 130 | return connection.on_line_build_attempt(self, points) 131 | def on_hit(self, hit_amount, player, type, grenade): 132 | if self.jailed: 133 | if self.name == player.name: 134 | self.send_chat("Suicide isn't an option!") 135 | return False 136 | else: 137 | self.send_chat("You can't hit people when you're jailed! You were jailed for: %s" % (self.reason)) 138 | return False 139 | return connection.on_hit(self, hit_amount, player, type, grenade) 140 | def on_disconnect(self): 141 | if self.jailed: 142 | jail_list.remove(self.name) 143 | self.jailed = False 144 | return connection.on_disconnect(self) 145 | return protocol, JailConnection 146 | -------------------------------------------------------------------------------- /mapmakingtools.py: -------------------------------------------------------------------------------- 1 | from commands import add, admin 2 | from pyspades.contained import BlockAction, SetColor 3 | from pyspades.constants import * 4 | from math import * 5 | 6 | EAST = 0 7 | SOUTH = 1 8 | WEST = 2 9 | NORTH = 3 10 | 11 | def make_color(r, g, b, a): 12 | return b | (g << 8) | (r << 16) | (int((a / 255.0) * 128) << 24) 13 | 14 | def make_color_tuple(color): 15 | return make_color(color[0], color[1], color[2], 255) 16 | 17 | def get_color_tuple(color): 18 | b = color & 0xFF 19 | g = (color & 0xFF00) >> 8 20 | r = (color & 0xFF0000) >> 16 21 | a = int((((color & 0xFF000000) >> 24) / 128.0) * 255) 22 | return (r, g, b, a) 23 | 24 | def set_color(prt, color, player_id = 32): 25 | c = SetColor() 26 | c.player_id = player_id 27 | c.value = color 28 | prt.send_contained(c) 29 | 30 | def add_block(prt, x, y, z, color, player_id = 32, mirror_x = False, mirror_y = False): 31 | if x >= 0 and x < 512 and y >= 0 and y < 512 and z >= 0 and z < 64: 32 | if mirror_x == True or mirror_y == True: 33 | x2 = x 34 | y2 = y 35 | if mirror_x == True: 36 | x2 = 511 - x 37 | if mirror_y == True: 38 | y2 = 511 - y 39 | add_block(prt, x2, y2, z, color, player_id, False, False) 40 | if not prt.map.get_solid(x, y, z): 41 | block_action = BlockAction() 42 | block_action.player_id = player_id 43 | block_action.value = BUILD_BLOCK 44 | block_action.x = x 45 | block_action.y = y 46 | block_action.z = z 47 | prt.send_contained(block_action) 48 | prt.map.set_point(x, y, z, get_color_tuple(color)) 49 | 50 | def remove_block(prt, x, y, z, mirror_x = False, mirror_y = False): 51 | if x >= 0 and x < 512 and y >= 0 and y < 512 and z >= 0 and z < 64: 52 | if mirror_x == True or mirror_y == True: 53 | x2 = x 54 | y2 = y 55 | if mirror_x == True: 56 | x2 = 511 - x 57 | if mirror_y == True: 58 | y2 = 511 - y 59 | remove_block(prt, x2, y2, z, False, False) 60 | if prt.map.get_solid(x, y, z): 61 | block_action = BlockAction() 62 | block_action.player_id = 32 63 | block_action.value = DESTROY_BLOCK 64 | block_action.x = x 65 | block_action.y = y 66 | block_action.z = z 67 | prt.map.remove_point(x, y, z) 68 | prt.send_contained(block_action) 69 | return True 70 | return False 71 | 72 | def mirror(connection, mirror_x, mirror_y): 73 | connection.mirror_x = bool(mirror_x) 74 | connection.mirror_y = bool(mirror_y) 75 | 76 | add(mirror) 77 | 78 | def tunnel(*arguments): 79 | connection = arguments[0] 80 | connection.reset_build() 81 | connection.callback = tunnel_r 82 | connection.arguments = arguments 83 | connection.select = True 84 | connection.points = 1 85 | 86 | add(tunnel) 87 | 88 | def tunnel_r(connection, radius, length, zoffset = 0): 89 | radius = int(radius) 90 | length = int(length) 91 | zoffset = int(zoffset) 92 | facing = connection.get_direction() 93 | if facing == WEST or facing == NORTH: 94 | length = -length 95 | for rel_h in xrange(-radius, radius + 1): 96 | for rel_v in xrange(-radius, 1): 97 | if round(sqrt(rel_h**2 + rel_v**2)) <= radius: 98 | if facing == NORTH or facing == SOUTH: 99 | y1 = connection.block1_y 100 | y2 = connection.block1_y + length 101 | for y in xrange(min(y1, y2), max(y1, y2) + 1): 102 | remove_block(connection.protocol, connection.block1_x + rel_h, y, connection.block1_z + rel_v + zoffset, connection.mirror_x, connection.mirror_y) 103 | elif facing == WEST or facing == EAST: 104 | x1 = connection.block1_x 105 | x2 = connection.block1_x + length 106 | for x in xrange(min(x1, x2), max(x1, x2) + 1): 107 | remove_block(connection.protocol, x, connection.block1_y + rel_h, connection.block1_z + rel_v + zoffset, connection.mirror_x, connection.mirror_y) 108 | 109 | def insert(*arguments): 110 | connection = arguments[0] 111 | connection.reset_build() 112 | connection.callback = insert_r 113 | connection.arguments = arguments 114 | connection.select = True 115 | connection.points = 2 116 | 117 | add(insert) 118 | 119 | def insert_r(connection): 120 | x1 = min(connection.block1_x, connection.block2_x) 121 | x2 = max(connection.block1_x, connection.block2_x) 122 | y1 = min(connection.block1_y, connection.block2_y) 123 | y2 = max(connection.block1_y, connection.block2_y) 124 | z1 = min(connection.block1_z, connection.block2_z) 125 | z2 = max(connection.block1_z, connection.block2_z) 126 | color = make_color_tuple(connection.color) 127 | for xx in xrange(x1, x2 + 1): 128 | for yy in xrange(y1, y2 + 1): 129 | for zz in xrange(z1, z2 + 1): 130 | add_block(connection.protocol, xx, yy, zz, color, connection.player_id, connection.mirror_x, connection.mirror_y) 131 | 132 | def delete(*arguments): 133 | connection = arguments[0] 134 | connection.reset_build() 135 | connection.callback = delete_r 136 | connection.arguments = arguments 137 | connection.select = True 138 | connection.points = 2 139 | 140 | add(delete) 141 | 142 | def delete_r(connection): 143 | x1 = min(connection.block1_x, connection.block2_x) 144 | x2 = max(connection.block1_x, connection.block2_x) 145 | y1 = min(connection.block1_y, connection.block2_y) 146 | y2 = max(connection.block1_y, connection.block2_y) 147 | z1 = min(connection.block1_z, connection.block2_z) 148 | z2 = max(connection.block1_z, connection.block2_z) 149 | for xx in xrange(x1, x2 + 1): 150 | for yy in xrange(y1, y2 + 1): 151 | for zz in xrange(z1, z2 + 1): 152 | remove_block(connection.protocol, xx, yy, zz, connection.mirror_x, connection.mirror_y) 153 | 154 | def pattern(*arguments): 155 | connection = arguments[0] 156 | connection.reset_build() 157 | connection.callback = pattern_r 158 | connection.arguments = arguments 159 | connection.select = True 160 | connection.points = 2 161 | 162 | add(pattern) 163 | 164 | def pattern_r(connection, copies): 165 | copies = int(copies) 166 | x1 = min(connection.block1_x, connection.block2_x) 167 | x2 = max(connection.block1_x, connection.block2_x) 168 | y1 = min(connection.block1_y, connection.block2_y) 169 | y2 = max(connection.block1_y, connection.block2_y) 170 | z1 = min(connection.block1_z, connection.block2_z) 171 | z2 = max(connection.block1_z, connection.block2_z) 172 | delta_z = (z2 - z1) + 1 173 | for xx in xrange(x1, x2 + 1): 174 | for yy in xrange(y1, y2 + 1): 175 | for zz in xrange(z1, z2 + 1): 176 | if connection.protocol.map.get_solid(xx, yy, zz): 177 | color = make_color_tuple(connection.protocol.map.get_point(xx, yy, zz)[1]) 178 | set_color(connection.protocol, color, 32) 179 | for i in xrange(1, copies + 1): 180 | z_offset = delta_z * i 181 | add_block(connection.protocol, xx, yy, zz - z_offset, color, 32, connection.mirror_x, connection.mirror_y) 182 | 183 | def hollow(*arguments): 184 | connection = arguments[0] 185 | connection.reset_build() 186 | connection.callback = hollow_r 187 | connection.arguments = arguments 188 | connection.select = True 189 | connection.points = 2 190 | 191 | add(hollow) 192 | 193 | def hollow_r(connection, thickness = 1): 194 | m = connection.protocol.map 195 | thickness = int(thickness) - 1 196 | x1 = min(connection.block1_x, connection.block2_x) 197 | x2 = max(connection.block1_x, connection.block2_x) 198 | y1 = min(connection.block1_y, connection.block2_y) 199 | y2 = max(connection.block1_y, connection.block2_y) 200 | z1 = min(connection.block1_z, connection.block2_z) 201 | z2 = max(connection.block1_z, connection.block2_z) 202 | blocks = [] 203 | xr = x2 - x1 + 1 204 | yr = y2 - y1 + 1 205 | zr = z2 - z1 + 1 206 | for x in xrange(0, xr): 207 | blocks.append([]) 208 | for y in xrange(0, yr): 209 | blocks[x].append([]) 210 | for z in xrange(0, zr): 211 | blocks[x][y].append(False) 212 | def hollow_check(xc, yc, zc, thickness): 213 | if thickness > 0: 214 | for xx in xrange(xc - 1, xc + 2): 215 | if xx >= 0 and xx < xr: 216 | for yy in xrange(yc - 1, yc + 2): 217 | if yy >= 0 and yy < yr: 218 | for zz in xrange(zc - 1, zc + 2): 219 | if zz >= 0 and zz < zr: 220 | blocks[xx][yy][zz] = True 221 | if m.get_solid(x1 + xx, y1 + yy, z1 + zz): 222 | hollow_check(xx, yy, zz, thickness - 1) 223 | for x in xrange(0, xr): 224 | for y in xrange(0, yr): 225 | for z in xrange(0, zr): 226 | if m.get_solid(x1 + x, y1 + y, z1 + z): 227 | if m.is_surface(x1 + x, y1 + y, z1 + z): 228 | blocks[x][y][z] = True 229 | hollow_check(x, y, z, thickness) 230 | else: 231 | blocks[x][y][z] = True 232 | for x in xrange(0, xr): 233 | for y in xrange(0, yr): 234 | for z in xrange(0, zr): 235 | if not blocks[x][y][z]: 236 | remove_block(connection.protocol, x1 + x, y1 + y, z1 + z) 237 | 238 | def apply_script(protocol, connection, config): 239 | class MapMakingToolsConnection(connection): 240 | select = False 241 | mirror_x = False 242 | mirror_y = False 243 | 244 | def reset_build(self): 245 | self.block1_x = None 246 | self.block1_y = None 247 | self.block1_z = None 248 | self.block2_x = None 249 | self.block2_y = None 250 | self.block2_z = None 251 | self.callback = None 252 | self.arguments = None 253 | self.select = False 254 | self.points = None 255 | 256 | def get_direction(self): 257 | orientation = self.world_object.orientation 258 | angle = atan2(orientation.y, orientation.x) 259 | if angle < 0: 260 | angle += 6.283185307179586476925286766559 261 | # Convert to units of quadrents 262 | angle *= 0.63661977236758134307553505349006 263 | angle = round(angle) 264 | if angle == 4: 265 | angle = 0 266 | return angle 267 | 268 | def on_block_destroy(self, x, y, z, value): 269 | if self.select == True: 270 | if self.points == 1: 271 | self.block1_x = x 272 | self.block1_y = y 273 | self.block1_z = z 274 | self.callback(*self.arguments) 275 | self.reset_build() 276 | return False 277 | elif self.points == 2: 278 | if self.block1_x == None: 279 | self.block1_x = x 280 | self.block1_y = y 281 | self.block1_z = z 282 | self.send_chat('First block selected') 283 | return False 284 | else: 285 | if not (x == self.block1_x and y == self.block1_y and z == self.block1_z): 286 | self.block2_x = x 287 | self.block2_y = y 288 | self.block2_z = z 289 | self.callback(*self.arguments) 290 | self.reset_build() 291 | self.send_chat('Second block selected') 292 | return False 293 | if self.mirror_x == True or self.mirror_y == True: 294 | x2 = x 295 | y2 = y 296 | if self.mirror_x == True: 297 | x2 = 511 - x 298 | if self.mirror_y == True: 299 | y2 = 511 - y 300 | remove_block(self.protocol, x2, y2, z) 301 | connection.on_block_destroy(self, x, y, z, value) 302 | 303 | def on_block_build(self, x, y, z): 304 | if self.mirror_x == True or self.mirror_y == True: 305 | x2 = x 306 | y2 = y 307 | if self.mirror_x == True: 308 | x2 = 511 - x 309 | if self.mirror_y == True: 310 | y2 = 511 - y 311 | add_block(self.protocol, x2, y2, z, make_color_tuple(self.color), self.player_id) 312 | connection.on_block_build(self, x, y, z) 313 | 314 | return protocol, MapMakingToolsConnection -------------------------------------------------------------------------------- /melee.py: -------------------------------------------------------------------------------- 1 | """ 2 | Melee game mode. 3 | 4 | Toggle with command: /melee 5 | 6 | Melee kills only. Guns and grenades are disabled. 7 | Holding a block shields you from melee attacks except when you're holding the enemy's intel. 8 | """ 9 | 10 | from pyspades.server import orientation_data, weapon_reload 11 | from pyspades.constants import * 12 | from commands import add, admin, name 13 | 14 | @admin 15 | @name('melee') 16 | def meleetoggle(connection): 17 | protocol = connection.protocol 18 | protocol.melee_mode = not protocol.melee_mode 19 | connection.send_chat("Melee is %s" % ['off', 'on'][protocol.melee_mode]) 20 | 21 | add(meleetoggle) 22 | 23 | def apply_script(protocol, connection, config): 24 | class MeleeProtocol(protocol): 25 | def __init__(self, *arg, **kw): 26 | protocol.__init__(self, *arg, **kw) 27 | self.melee_mode = True 28 | 29 | class MeleeConnection(connection): 30 | def on_hit(self, hit_amount, hit_player, type, grenade): 31 | if self.protocol.melee_mode and (type != MELEE_KILL or (hit_player.tool == BLOCK_TOOL and hit_player.has_intel == False)): 32 | return False 33 | return connection.on_hit(self, hit_amount, hit_player, type, grenade) 34 | 35 | def on_grenade(self, time_left): 36 | if self.protocol.melee_mode: 37 | return False 38 | 39 | def on_flag_capture(self): 40 | self.has_intel = False 41 | return connection.on_flag_capture(self) 42 | 43 | def on_flag_drop(self): 44 | self.has_intel = False 45 | return connection.on_flag_drop(self) 46 | 47 | def on_flag_take(self): 48 | self.has_intel = True 49 | return connection.on_flag_take(self) 50 | 51 | def on_join(self): 52 | self.has_intel = False 53 | return connection.on_join(self) 54 | 55 | def on_spawn(self, pos): 56 | if self.protocol.melee_mode: 57 | self.clear_ammo() 58 | 59 | def on_refill(self): 60 | if not self.protocol.melee_mode: 61 | weapon_reload.player_id = self.player_id 62 | weapon_reload.clip_ammo = self.weapon_object.current_ammo 63 | weapon_reload.reserve_ammo = self.weapon_object.current_stock 64 | self.send_contained(weapon_reload) 65 | 66 | self.weapon_object.reload() 67 | return connection.on_refill(self) 68 | 69 | # clear_ammo() method by infogulch 70 | def clear_ammo(self): 71 | weapon_reload.player_id = self.player_id 72 | weapon_reload.clip_ammo = 0 73 | weapon_reload.reserve_ammo = 0 74 | self.grenades = 0 75 | self.weapon_object.clip_ammo = 0 76 | self.weapon_object.reserve_ammo = 0 77 | self.send_contained(weapon_reload) 78 | 79 | return MeleeProtocol, MeleeConnection 80 | -------------------------------------------------------------------------------- /meleerotation.py: -------------------------------------------------------------------------------- 1 | """ 2 | MeleeRotation 3 | Melee round every N rounds. 4 | 5 | Requires melee.py to be loaded. 6 | 7 | ISSUES: 8 | 1. The first N rounds are melee unless the mode is manually unset 9 | when meleerotation is below melee in config.txt 10 | 2. Players must return to their tentthings once melee mode is off 11 | to be able to shoot. 12 | """ 13 | 14 | ROTATE_EVERY = 3 15 | 16 | def apply_script(protocol, connection, config): 17 | class MeleeRotationProtocol(protocol): 18 | def __init__(self, *arg, **kw): 19 | self.melee_mode = False 20 | self.round = 1 21 | protocol.__init__(self, *arg, **kw) 22 | 23 | class MeleeRotationConnection(connection): 24 | def on_flag_capture(self): 25 | protocol = self.protocol 26 | 27 | self.has_intel = False 28 | protocol.round += 1 29 | 30 | if protocol.round % ROTATE_EVERY == 0: 31 | protocol.melee_mode = True 32 | protocol.send_chat("This is a melee round!") 33 | elif protocol.round % ROTATE_EVERY == 1: 34 | protocol.melee_mode = False 35 | protocol.send_chat("The melee round is over.") 36 | 37 | return connection.on_flag_capture(self) 38 | 39 | return MeleeRotationProtocol, MeleeRotationConnection 40 | -------------------------------------------------------------------------------- /onectf.py: -------------------------------------------------------------------------------- 1 | from pyspades.constants import * 2 | from commands import add, admin 3 | from pyspades.collision import vector_collision 4 | 5 | FLAG_SPAWN_POS = (256, 256) 6 | 7 | HIDE_POS = (0, 0, 63) 8 | 9 | # 1CTF and R1CTF can be enabled via the map metadata. Enable it by setting 10 | # 'one_ctf' to 'True' or 'reverse_one_ctf' to 'True' in the extensions dictionary. ex: 11 | # extensions = {'reverse_one_ctf': True} 12 | 13 | DISABLED, ONE_CTF, REVERSE_ONE_CTF = xrange(3) 14 | 15 | ONE_CTF_MODE = REVERSE_ONE_CTF 16 | 17 | # In reverse ctf, the goal is to take the intel to the enemy base 18 | 19 | # The message to send when a player takes the intel to the wrong base 20 | # when playing reverse ctf 21 | REVERSE_ONE_CTF_MESSAGE = 'Take the intel to the enemy base to score.' 22 | 23 | def apply_script(protocol, connection, config): 24 | game_mode = config.get('game_mode', 'ctf') 25 | if game_mode != 'ctf': 26 | return protocol, connection 27 | 28 | class OneCTFProtocol(protocol): 29 | one_ctf = False 30 | reverse_one_ctf = False 31 | 32 | def onectf_reset_flag(self, flag): 33 | z = self.map.get_z(*self.one_ctf_spawn_pos) 34 | pos = (self.one_ctf_spawn_pos[0], self.one_ctf_spawn_pos[1], z) 35 | if flag is not None: 36 | flag.player = None 37 | flag.set(*pos) 38 | flag.update() 39 | return pos 40 | 41 | def onectf_reset_flags(self): 42 | if self.one_ctf or self.reverse_one_ctf: 43 | self.onectf_reset_flag(self.blue_team.flag) 44 | self.onectf_reset_flag(self.green_team.flag) 45 | 46 | def on_game_end(self): 47 | if self.one_ctf or self.reverse_one_ctf: 48 | self.onectf_reset_flags() 49 | return protocol.on_game_end(self) 50 | 51 | def on_map_change(self, map): 52 | self.one_ctf = self.reverse_one_ctf = False 53 | self.one_ctf_spawn_pos = FLAG_SPAWN_POS 54 | extensions = self.map_info.extensions 55 | if ONE_CTF_MODE == ONE_CTF: 56 | self.one_ctf = True 57 | elif ONE_CTF_MODE == REVERSE_ONE_CTF: 58 | self.reverse_one_ctf = True 59 | elif extensions.has_key('one_ctf'): 60 | self.one_ctf = extensions['one_ctf'] 61 | if not self.one_ctf and extensions.has_key('reverse_one_ctf'): 62 | self.reverse_one_ctf = extensions['reverse_one_ctf'] 63 | if extensions.has_key('one_ctf_spawn_pos'): 64 | self.one_ctf_spawn_pos = extensions['one_ctf_spawn_pos'] 65 | return protocol.on_map_change(self, map) 66 | 67 | def on_flag_spawn(self, x, y, z, flag, entity_id): 68 | pos = self.onectf_reset_flag(flag.team.other.flag) 69 | protocol.on_flag_spawn(self, pos[0], pos[1], pos[2], flag, entity_id) 70 | return pos 71 | 72 | class OneCTFConnection(connection): 73 | def on_flag_take(self): 74 | if self.protocol.one_ctf or self.protocol.reverse_one_ctf: 75 | flag = self.team.flag 76 | if flag.player is None: 77 | flag.set(*HIDE_POS) 78 | flag.update() 79 | if self.protocol.reverse_one_ctf: 80 | self.send_chat(REVERSE_ONE_CTF_MESSAGE) 81 | else: 82 | return False 83 | return connection.on_flag_take(self) 84 | 85 | def on_flag_drop(self): 86 | if self.protocol.one_ctf or self.protocol.reverse_one_ctf: 87 | flag = self.team.flag 88 | position = self.world_object.position 89 | x, y, z = int(position.x), int(position.y), max(0, int(position.z)) 90 | z = self.protocol.map.get_z(x, y, z) 91 | flag.set(x, y, z) 92 | flag.update() 93 | return connection.on_flag_drop(self) 94 | 95 | def on_position_update(self): 96 | if self.protocol.reverse_one_ctf: 97 | if vector_collision(self.world_object.position, self.team.other.base): 98 | other_flag = self.team.other.flag 99 | if other_flag.player is self: 100 | connection.capture_flag(self) 101 | return connection.on_position_update(self) 102 | 103 | def capture_flag(self): 104 | if self.protocol.reverse_one_ctf: 105 | self.send_chat(REVERSE_ONE_CTF_MESSAGE) 106 | return False 107 | return connection.capture_flag(self) 108 | 109 | def on_flag_capture(self): 110 | if self.protocol.one_ctf or self.protocol.reverse_one_ctf: 111 | self.protocol.onectf_reset_flags() 112 | return connection.on_flag_capture(self) 113 | 114 | return OneCTFProtocol, OneCTFConnection -------------------------------------------------------------------------------- /quickbuild.py: -------------------------------------------------------------------------------- 1 | """ 2 | Build structures instantly, and easily record new ones! 3 | 4 | See: https://github.com/infogulch/pyspades-userscripts/wiki/Quickbuild 5 | """ 6 | 7 | from commands import add, alias, admin 8 | from math import floor, atan2, pi 9 | from pyspades.constants import * 10 | from pyspades.contained import BlockAction, SetColor 11 | from pyspades.common import make_color 12 | from itertools import imap, izip 13 | import json 14 | import cbc 15 | import collections 16 | import itertools 17 | import os 18 | import re 19 | from avx import AVX 20 | 21 | QB_DIR = './qb' 22 | 23 | DIRT_COLOR = (103, 64, 40) 24 | 25 | EAST, SOUTH, WEST, NORTH = xrange(4) 26 | 27 | # recording modes 28 | Q_STOPPED, Q_RECORDING, Q_COPYING, Q_ORIGINATING = xrange(4) 29 | 30 | # build modes 31 | Q_OFF, Q_BUILD, Q_BUILD_RECORDED = xrange(3) 32 | 33 | if not os.path.exists(QB_DIR): 34 | os.makedirs(QB_DIR) 35 | 36 | def shift_origin_all(dct, new_origin): 37 | # dct is a dict (or another 2-tuple iterable) of tuple, color 38 | # returns a 2-tuple iterator 39 | # new_origin is relative to the current origin. 40 | # the old origin is now the inverse of new_origin 41 | new_origin = tuple(new_origin) 42 | shift = lambda tpl: tuple(a-b for a,b in zip(tpl, new_origin)) 43 | if isinstance(dct, dict): 44 | dct = dct.iteritems() 45 | for k,v in dct: 46 | yield shift(k), v 47 | 48 | def shift_origin(coords, new_origin): 49 | return shift_origin_all(((coords, None),), new_origin).next()[0] 50 | 51 | def rotate_all(dct, fm, to): 52 | # dct is a dict (or 2-tuple iterator) of tuple, color 53 | # returns a 2-tuple iterator that shifts the original dict about the origin (0,0,0) 54 | # assumes y increases to the south 55 | amt = (to - fm) % 4 56 | if amt == 0: 57 | rot = lambda t: t 58 | elif amt == 1: 59 | rot = lambda t: (-t[1], t[0]) + t[2:] 60 | elif amt == 2: 61 | rot = lambda t: (-t[0], -t[1]) + t[2:] 62 | elif amt == 3: 63 | rot = lambda t: ( t[1], -t[0]) + t[2:] 64 | if isinstance(dct, dict): 65 | dct = dct.iteritems() 66 | for k,v in dct: 67 | yield rot(k), v 68 | 69 | def rotate(coords, fm, to): 70 | return rotate_all(((coords, None),), fm, to).next()[0] 71 | 72 | def get_blocks(vmap, xyz1, xyz2, colors = False): 73 | p = [xrange(a,b+1) for a,b in map(sorted, zip(xyz1, xyz2))] 74 | for xyz in itertools.product(*p): 75 | solid, color = vmap.get_point(*xyz) 76 | if solid: 77 | yield xyz, color if colors else None 78 | 79 | def qb_fname(name): 80 | if not re.match('\w+$', name): 81 | return None 82 | fname = '%s/%s.avx' % (QB_DIR, name) 83 | return fname 84 | 85 | def qb_get_info(fname): 86 | try: 87 | settings = json.load(open(fname, 'r')) 88 | except (IOError, ValueError), e: 89 | settings = {} 90 | return settings 91 | 92 | def qb_update_info(fname, info): 93 | current = qb_get_info(fname) 94 | current.update(info) 95 | json.dump(current, open(fname, 'w')) 96 | 97 | # Use these commands to create quickbuild structures. 98 | @admin 99 | def qbrecord(connection, colored = ''): 100 | if connection.qb_recording: 101 | connection.qb_recording = Q_STOPPED 102 | return 'No longer recording.' 103 | else: 104 | s_colors = 'WITHOUT' 105 | s_origin = ' First block sets the origin and orientation.' 106 | connection.qb_recording = Q_RECORDING 107 | connection.qb_record_colors = colored.lower() == 'colored' 108 | if connection.qb_record_colors: 109 | s_colors = 'WITH' 110 | if connection.qb_record_origin: 111 | s_origin = '' 112 | return 'Now recording %s colors.%s' % (s_colors, s_origin) 113 | 114 | @admin 115 | def qbsave(connection, name, cost = None, *description): 116 | fname = qb_fname(name) 117 | if not fname: 118 | return 'Invalid save name. Only alphanumeric characters allowed.' 119 | if not connection.qb_recorded: 120 | return 'Nothing is recorded yet!' 121 | 122 | shift = map(min, zip(*connection.qb_recorded.iterkeys())) 123 | origin = [-x for x in shift] 124 | 125 | info = {'colored': connection.qb_record_colors 126 | , 'origin': origin} 127 | if cost is not None: 128 | info['cost'] = int(cost) 129 | if description: 130 | info['description'] = ' '.join(description) 131 | 132 | qb_update_info(fname + '.txt', info) 133 | 134 | recorded = dict(shift_origin_all(connection.qb_recorded, shift)) 135 | AVX.fromsparsedict(recorded, info['colored']).save(fname) 136 | 137 | return 'Saved buffer to %s.avx' % name 138 | 139 | @admin 140 | def qbload(connection, name): 141 | fname = qb_fname(name) 142 | if not fname: 143 | return 'Invalid load name. Only alphanumeric characters allowed.' 144 | qbclear(connection) 145 | 146 | settings = qb_get_info(fname + '.txt') 147 | 148 | if not settings.has_key('origin'): 149 | return 'Invalid information for file %s' % name 150 | 151 | recorded = AVX.fromfile(fname).tosparsedict() 152 | 153 | connection.qb_recorded = dict(shift_origin_all(recorded, settings['origin'])) 154 | 155 | return 'Loaded %s.avx to buffer.' % name 156 | 157 | @admin 158 | def qbprint(connection): 159 | print connection.qb_recorded 160 | 161 | @admin 162 | def qbrotate(connection, amount): 163 | amount = int(amount) 164 | if amount in xrange(4): 165 | connection.qb_recorded = dict(rotate_all(connection.qb_recorded, 0, amount)) 166 | 167 | @admin 168 | def qbshiftorigin(connection): 169 | if not connection.qb_recording: 170 | return 'You must be recording first.' 171 | if not connection.qb_record_origin or not connection.qb_recorded: 172 | return 'Nothing recorded yet!' 173 | if connection.qb_recording == Q_ORIGINATING: 174 | connection.qb_recording = Q_STOPPED 175 | return 'No longer changing the origin. Recording stopped.' 176 | connection.qb_recording = Q_ORIGINATING 177 | return 'Place a block to mark the new origin AND orientation!' 178 | 179 | @admin 180 | def qbcopy(connection, colored = ''): 181 | qbclear(connection) 182 | connection.qb_recording = Q_COPYING 183 | connection.qb_record_colors = colored.lower() == 'colored' 184 | s_colors = 'WITH' if connection.qb_record_colors else 'WITHOUT' 185 | return 'Copying %s colors. Place first corner block. This sets the the origin and orientation.' % s_colors 186 | 187 | @admin 188 | def qbclear(connection): 189 | message = 'Quickbuild recorded blocks cleared.' 190 | if connection.qb_recording: 191 | message += ' Recording stopped.' 192 | connection.qb_recorded.clear() 193 | connection.qb_record_origin = None 194 | connection.qb_record_colors = False 195 | connection.qb_recording = Q_STOPPED 196 | return message 197 | 198 | @alias('br') 199 | @admin 200 | def buildrecorded(connection): 201 | message = 'The next block you place will build the recorded structure.' 202 | if connection.qb_recording != Q_STOPPED: 203 | message += ' Recording stopped.' 204 | connection.qb_recording = Q_STOPPED 205 | connection.qb_building = Q_BUILD_RECORDED 206 | connection.qb_info = None 207 | return message 208 | 209 | # Build a structure. 210 | @alias('b') 211 | def build(connection, name = None): 212 | if not connection.quickbuild_allowed: 213 | return 'You are not allowed to build' 214 | if name is None: 215 | connection.qb_building = Q_OFF 216 | connection.qb_info = None 217 | sts = '' 218 | for fname in glob.iglob(QB_DIR + '/*.avx.txt'): 219 | info = qb_get_info(fname) 220 | if not info.has_key('cost'): 221 | continue 222 | st = ', %s(%i)' % (name, info['cost']) 223 | if len(sts + st) >= MAX_CHAT_SIZE: 224 | connection.send_chat(sts[2:]) 225 | sts = '' 226 | sts += st 227 | connection.send_chat(sts[2:]) 228 | return 'QuickBuild: Available structures. NAME(cost). /build NAME. You have %i points.' % connection.qb_points 229 | else: 230 | if connection.qb_info != None: 231 | connection.qb_building = Q_OFF 232 | connection.qb_info = None 233 | return "No longer building." 234 | fname = qb_fname(name) 235 | if fname is None: 236 | return 'Invalid structure name' 237 | info = qb_get_info(fname + '.txt') 238 | if not connection.god and not info.has_key('cost'): 239 | return 'Invalid structure' 240 | cost = info.get('cost', 0) 241 | info['name'] = name 242 | if connection.qb_points >= cost or connection.god: 243 | connection.qb_info = info 244 | connection.qb_building = Q_BUILD 245 | return 'The next block you place will build a %s.' % info.get('description', name) 246 | else: 247 | return ('You need %i more points if you want to build %s: %s.' 248 | % (cost-connection.qb_points, name, info.get('description', name))) 249 | 250 | add(build) 251 | add(buildrecorded) 252 | 253 | add(qbrecord) 254 | add(qbsave) 255 | add(qbload) 256 | add(qbclear) 257 | # add(qbprint) 258 | 259 | add(qbcopy) 260 | add(qbrotate) 261 | add(qbshiftorigin) 262 | 263 | def apply_script(protocol, connection, config): 264 | protocol, connection = cbc.apply_script(protocol, connection, config) 265 | 266 | class BuildConnection(connection): 267 | def __init__(self, *arg, **kw): 268 | connection.__init__(self, *arg, **kw) 269 | self.quickbuild_allowed = True 270 | self.qb_info = None 271 | self.qb_points = 0 272 | self.qb_building = Q_OFF 273 | 274 | self.qb_recording = Q_STOPPED 275 | self.qb_record_colors = False 276 | self.qb_record_origin = None 277 | self.qb_record_dir = None 278 | self.qb_recorded = dict() 279 | 280 | def get_direction(self): 281 | orientation = self.world_object.orientation 282 | return int(round(atan2(orientation.y, orientation.x) / pi * 2) % 4) 283 | 284 | def on_kill(self, killer, type, grenade): 285 | ret = connection.on_kill(self, killer, type, grenade) 286 | if ret is None: 287 | if killer is not None and self.team is not killer.team and self != killer: 288 | killer.qb_points += 1 289 | return ret 290 | 291 | def on_line_build(self, points): 292 | for x, y, z in points: 293 | self.quickbuild_block_build(x, y, z) 294 | return connection.on_line_build(self, points) 295 | 296 | def on_block_build(self, x, y, z): 297 | self.quickbuild_block_build(x, y, z) 298 | return connection.on_block_build(self, x, y, z) 299 | 300 | def on_block_removed(self, x, y, z): 301 | connection.on_block_removed(self, x, y, z) 302 | if self.qb_recording and self.qb_recorded: 303 | xyz = shift_origin((x,y,z),self.qb_record_origin) 304 | self.qb_recorded.pop(xyz, 'Not popped') 305 | 306 | def quickbuild_block_build(self, x, y, z): 307 | if self.qb_recording: 308 | if self.qb_record_origin is None: 309 | self.qb_record_origin = (x, y, z) 310 | self.qb_record_dir = self.get_direction() 311 | xyz = shift_origin((x,y,z), self.qb_record_origin) 312 | xyz = rotate(xyz, self.qb_record_dir, EAST) 313 | self.qb_recorded[xyz] = self.color if self.qb_record_colors else None 314 | 315 | def on_block_build_attempt(self, x, y, z): 316 | cont = True 317 | if self.qb_recording and not self.qb_record_origin: 318 | self.qb_record_origin = (x, y, z) 319 | self.qb_record_dir = self.get_direction() 320 | if self.qb_recording == Q_COPYING: 321 | self.send_chat('Now place the opposite corner block!') 322 | else: 323 | self.send_chat('Now start building!') 324 | cont = False 325 | elif self.qb_recording == Q_COPYING: 326 | blocks = get_blocks(self.protocol.map, self.qb_record_origin, (x,y,z), self.qb_record_colors) 327 | blocks = shift_origin_all(blocks, self.qb_record_origin) 328 | blocks = rotate_all(blocks, self.qb_record_dir, EAST) 329 | self.qb_recorded = dict(blocks) 330 | self.qb_recording = Q_STOPPED 331 | self.send_chat('Copied area to buffer!') 332 | cont = False 333 | 334 | if self.qb_building: 335 | if self.qb_building == Q_BUILD_RECORDED: 336 | structure = rotate_all(self.qb_recorded, EAST, self.get_direction()) 337 | color = DIRT_COLOR if self.qb_record_colors else self.color 338 | else: 339 | self.qb_points -= self.qb_info.get('cost', 0) 340 | vx = AVX.fromfile(qb_fname(self.qb_info['name'])) 341 | structure = vx.tosparsedict() 342 | structure = shift_origin_all(structure, self.qb_info['origin']) 343 | structure = rotate_all(structure, EAST, self.get_direction()) 344 | color = DIRT_COLOR if vx.has_colors else self.color 345 | self.protocol.cbc_add(self.quickbuild_generator((x, y, z), structure, color)) 346 | self.qb_building = Q_OFF 347 | self.qb_info = None 348 | self.send_chat('Building structure!') 349 | cont = False 350 | 351 | if self.qb_recording == Q_ORIGINATING: 352 | new_origin = (x,y,z) 353 | shift = shift_origin(self.qb_record_origin, new_origin) 354 | self.qb_recorded = dict(shift_origin_all(self.qb_recorded, shift)) 355 | self.qb_record_origin = new_origin 356 | self.send_chat('New origin saved!') 357 | cont = False 358 | 359 | if not cont: 360 | return False 361 | return connection.on_block_build_attempt(self, x, y, z) 362 | 363 | def quickbuild_generator(self, origin, structure, default_color): 364 | map = self.protocol.map 365 | protocol = self.protocol 366 | 367 | splayer = cbc.ServerPlayer() 368 | 369 | block_action = BlockAction() 370 | block_action.value = BUILD_BLOCK 371 | block_action.player_id = splayer.player_id 372 | 373 | set_color = SetColor() 374 | set_color.value = make_color(*default_color) 375 | set_color.player_id = splayer.player_id 376 | pcolor = default_color 377 | 378 | protocol.send_contained(set_color, save = True) 379 | 380 | if not isinstance(structure, dict): 381 | structure = dict(structure) 382 | 383 | for xyz, color in structure.iteritems(): 384 | x, y, z = [a+b for a,b in zip(xyz, origin)] 385 | if (x < 0 or x >= 512 or y < 0 or y >= 512 or z < 0 or z >= 62): 386 | continue 387 | if map.get_solid(x, y, z): 388 | continue 389 | color = color or default_color 390 | if color != pcolor: 391 | set_color.value = make_color(*color) 392 | protocol.send_contained(set_color, save = True) 393 | pcolor = color 394 | yield 1, 0 395 | self.on_block_build(x, y, z) 396 | block_action.x, block_action.y, block_action.z = x, y, z 397 | protocol.send_contained(block_action, save = True) 398 | map.set_point(x, y, z, pcolor) 399 | yield 1, 0 400 | 401 | return protocol, BuildConnection -------------------------------------------------------------------------------- /rampage.py: -------------------------------------------------------------------------------- 1 | """ 2 | Killing KILL_REQUIREMENT other players in under TIME_REQUIREMENT seconds 3 | sends you into a rampage lasting RAMPAGE_DURATION seconds. 4 | The rampage refills and drastically increases your weapons' rate of fire. 5 | 6 | By default this means 3 kills in under 8 seconds to activate. For reference, 7 | lines disappear from the killfeed 10 seconds after they appear. 8 | 9 | Intended for use in frantic last team standing or free for all matches. 10 | 11 | Maintainer: hompy 12 | """ 13 | 14 | from collections import deque 15 | from twisted.internet.reactor import callLater, seconds 16 | from twisted.internet.task import LoopingCall 17 | from pyspades.server import set_tool, fog_color, weapon_reload 18 | from pyspades.common import make_color 19 | from pyspades.constants import * 20 | 21 | KILL_REQUIREMENT = 3 22 | TIME_REQUIREMENT = 8.0 23 | GRENADE_KILLS_COUNT = True 24 | RAMPAGE_REFILLS = True 25 | RAMPAGE_RELOADS = True 26 | RAMPAGE_DURATION = 20.0 27 | RAPID_INTERVALS = { 28 | RIFLE_WEAPON : 0.16, 29 | SMG_WEAPON : 0.08, 30 | SHOTGUN_WEAPON : 0.18 31 | } 32 | RAMPAGE_FOG_COLOR = (255, 0, 0) 33 | RAMPAGE_FOG_FUNC = lambda: RAMPAGE_FOG_COLOR 34 | 35 | ANNOUNCE_RAMPAGE = True 36 | S_RAMPAGE_START = '{player} IS ON A RAMPAGE!!' 37 | S_RAMPAGE_KILLED = "{victim}'s rampage was ended by {killer}" 38 | 39 | def resend_tool(player): 40 | set_tool.player_id = player.player_id 41 | set_tool.value = player.tool 42 | if player.weapon_object.shoot: 43 | player.protocol.send_contained(set_tool) 44 | else: 45 | player.send_contained(set_tool) 46 | 47 | def rapid_cycle(player): 48 | resend_tool(player) 49 | if not player.weapon_object.shoot: 50 | player.rampage_rapid_loop.stop() 51 | 52 | def send_fog(player, color): 53 | fog_color.color = make_color(*color) 54 | player.send_contained(fog_color) 55 | 56 | def fog_switch(player, colorgetter_a, colorgetter_b): 57 | if player.rampage: 58 | send_fog(player, colorgetter_a()) 59 | player.rampage_warning_call = callLater(0.5, fog_switch, player, 60 | colorgetter_b, colorgetter_a) 61 | 62 | def apply_script(protocol, connection, config): 63 | class RampageConnection(connection): 64 | rampage = False 65 | rampage_rapid_loop = None 66 | rampage_call = None 67 | rampage_warning_call = None 68 | rampage_kills = None 69 | rampage_reenable_rapid_hack_detect = None 70 | 71 | def start_rampage(self): 72 | self.rampage = True 73 | self.rampage_kills.clear() 74 | self.rampage_reenable_rapid_hack_detect = self.rapid_hack_detect 75 | self.rapid_hack_detect = False 76 | self.rampage_call = callLater(RAMPAGE_DURATION, self.end_rampage) 77 | if RAMPAGE_DURATION > 4.0: 78 | self.rampage_warning_call = callLater(RAMPAGE_DURATION - 3.0, 79 | fog_switch, self, self.protocol.get_fog_color, 80 | RAMPAGE_FOG_FUNC) 81 | if RAMPAGE_REFILLS: 82 | self.refill() 83 | if RAMPAGE_RELOADS: 84 | weapon = self.weapon_object 85 | was_shooting = weapon.shoot 86 | weapon.reset() 87 | weapon_reload.player_id = self.player_id 88 | weapon_reload.clip_ammo = weapon.current_ammo 89 | weapon_reload.reserve_ammo = weapon.current_stock 90 | weapon.set_shoot(was_shooting) 91 | self.send_contained(weapon_reload) 92 | send_fog(self, RAMPAGE_FOG_COLOR) 93 | if ANNOUNCE_RAMPAGE: 94 | message = S_RAMPAGE_START.format(player = self.name) 95 | self.protocol.send_chat(message, global_message = None) 96 | 97 | def end_rampage(self): 98 | self.rampage = False 99 | self.rapid_hack_detect = self.rampage_reenable_rapid_hack_detect 100 | if self.rampage_call and self.rampage_call.active(): 101 | self.rampage_call.cancel() 102 | self.rampage_call = None 103 | if self.rampage_warning_call and self.rampage_warning_call.active(): 104 | self.rampage_warning_call.cancel() 105 | self.rampage_warning_call = None 106 | if self.rampage_rapid_loop and self.rampage_rapid_loop.running: 107 | self.rampage_rapid_loop.stop() 108 | send_fog(self, self.protocol.fog_color) 109 | 110 | def on_connect(self): 111 | self.rampage_rapid_loop = LoopingCall(rapid_cycle, self) 112 | self.rampage_kills = deque(maxlen = KILL_REQUIREMENT) 113 | connection.on_connect(self) 114 | 115 | def on_disconnect(self): 116 | if self.rampage: 117 | self.end_rampage() 118 | self.rampage_rapid_loop = None 119 | connection.on_disconnect(self) 120 | 121 | def on_reset(self): 122 | if self.rampage: 123 | self.end_rampage() 124 | connection.on_reset(self) 125 | 126 | def on_kill(self, killer, type, grenade): 127 | was_rampaging = self.rampage 128 | if self.rampage: 129 | self.end_rampage() 130 | if killer is not None and killer is not self: 131 | if was_rampaging and ANNOUNCE_RAMPAGE: 132 | message = S_RAMPAGE_KILLED.format(victim = self.name, 133 | killer = killer.name) 134 | self.protocol.send_chat(message, global_message = None) 135 | if (not killer.rampage and killer.hp and 136 | killer.team is not self.team and 137 | (GRENADE_KILLS_COUNT or type != GRENADE_KILL)): 138 | now = seconds() 139 | killer.rampage_kills.append(now) 140 | if (len(killer.rampage_kills) == KILL_REQUIREMENT and 141 | killer.rampage_kills[0] >= now - TIME_REQUIREMENT): 142 | killer.start_rampage() 143 | return connection.on_kill(self, killer, type, grenade) 144 | 145 | def on_grenade_thrown(self, grenade): 146 | if self.rampage: 147 | resend_tool(self) 148 | connection.on_grenade_thrown(self, grenade) 149 | 150 | def on_shoot_set(self, fire): 151 | if (self.rampage and fire and 152 | self.rampage_rapid_loop and not self.rampage_rapid_loop.running): 153 | interval = RAPID_INTERVALS[self.weapon] 154 | self.rampage_rapid_loop.start(interval, now = False) 155 | connection.on_shoot_set(self, fire) 156 | 157 | send_fog_rule = lambda player: not player.rampage 158 | 159 | class RampageProtocol(protocol): 160 | def set_fog_color(self, color): 161 | self.fog_color = color 162 | fog_color.color = make_color(*color) 163 | 164 | self.send_contained(fog_color, save = True, rule = send_fog_rule) 165 | 166 | return RampageProtocol, RampageConnection -------------------------------------------------------------------------------- /rapid.py: -------------------------------------------------------------------------------- 1 | """ 2 | /rapid [player] will put the player in rapid mode, speeding up all tools 3 | including weapons. 4 | 5 | RAPID_BLOCK_DELAY determines how fast block placement should be. 6 | Lowering this value is not recommended except for local use. Ping to the server 7 | is the effective lower limit: if this value is set to 0.1 (0.1 seconds = 100ms), 8 | users with ping above that won't have the same advantage as the rest of the 9 | players. 10 | 11 | Set ALWAYS_RAPID to TRUE to automatically get rapid when you login. 12 | 13 | Mantainer: hompy 14 | """ 15 | 16 | from twisted.internet.reactor import callLater 17 | from twisted.internet.task import LoopingCall 18 | from pyspades.server import set_tool 19 | from pyspades.constants import * 20 | from commands import add, admin, get_player, name 21 | 22 | ALWAYS_RAPID = False 23 | RAPID_INTERVAL = 0.08 24 | RAPID_BLOCK_DELAY = 0.26 25 | 26 | @name('rapid') 27 | @admin 28 | def toggle_rapid(connection, player = None): 29 | protocol = connection.protocol 30 | if player is not None: 31 | player = get_player(protocol, player) 32 | elif connection in protocol.players: 33 | player = connection 34 | else: 35 | raise ValueError() 36 | 37 | player.rapid = rapid = not player.rapid 38 | player.rapid_hack_detect = not rapid 39 | if rapid: 40 | player.rapid_loop = LoopingCall(resend_tool, player) 41 | else: 42 | if player.rapid_loop and player.rapid_loop.running: 43 | player.rapid_loop.stop() 44 | player.rapid_loop = None 45 | 46 | message = 'now rapid' if rapid else 'no longer rapid' 47 | player.send_chat("You're %s" % message) 48 | if connection is not player and connection in protocol.players: 49 | connection.send_chat('%s is %s' % (player.name, message)) 50 | protocol.irc_say('* %s is %s' % (player.name, message)) 51 | 52 | def resend_tool(player): 53 | set_tool.player_id = player.player_id 54 | set_tool.value = player.tool 55 | if player.weapon_object.shoot: 56 | player.protocol.send_contained(set_tool) 57 | else: 58 | player.send_contained(set_tool) 59 | 60 | add(toggle_rapid) 61 | 62 | @name('srap') 63 | @admin 64 | def toggle_rapid_silent(connection, player = None): 65 | protocol = connection.protocol 66 | if player is not None: 67 | player = get_player(protocol, player) 68 | elif connection in protocol.players: 69 | player = connection 70 | else: 71 | raise ValueError() 72 | 73 | player.rapid = rapid = not player.rapid 74 | player.rapid_hack_detect = not rapid 75 | if rapid: 76 | player.rapid_loop = LoopingCall(resend_tool, player) 77 | else: 78 | if player.rapid_loop and player.rapid_loop.running: 79 | player.rapid_loop.stop() 80 | player.rapid_loop = None 81 | 82 | message = 'now rapid' if rapid else 'no longer rapid' 83 | if connection is not player and connection in protocol.players: 84 | connection.send_chat('%s is %s' % (player.name, message)) 85 | protocol.irc_say('* %s is %s' % (player.name, message)) 86 | 87 | add(toggle_rapid_silent) 88 | 89 | 90 | def apply_script(protocol, connection, config): 91 | class RapidConnection(connection): 92 | rapid = False 93 | rapid_loop = None 94 | 95 | def on_login(self, name): 96 | self.rapid = ALWAYS_RAPID 97 | self.rapid_hack_detect = not self.rapid 98 | connection.on_login(self, name) 99 | 100 | def on_reset(self): 101 | if self.rapid_loop and self.rapid_loop.running: 102 | self.rapid_loop.stop() 103 | connection.on_reset(self) 104 | 105 | def on_disconnect(self): 106 | self.rapid = False 107 | if self.rapid_loop and self.rapid_loop.running: 108 | self.rapid_loop.stop() 109 | self.rapid_loop = None 110 | connection.on_disconnect(self) 111 | 112 | def on_block_build(self, x, y, z): 113 | if self.rapid: 114 | delay = max(0.0, RAPID_BLOCK_DELAY - self.latency / 1000.0) 115 | if delay > 0.0: 116 | callLater(delay, resend_tool, self) 117 | else: 118 | resend_tool(self) 119 | connection.on_block_build(self, x, y, z) 120 | 121 | def on_grenade_thrown(self, grenade): 122 | if self.rapid: 123 | resend_tool(self) 124 | connection.on_grenade_thrown(self, grenade) 125 | 126 | def on_shoot_set(self, fire): 127 | if self.rapid and self.rapid_loop: 128 | if not self.rapid_loop.running and fire: 129 | self.rapid_loop.start(RAPID_INTERVAL) 130 | elif self.rapid_loop.running and not fire: 131 | self.rapid_loop.stop() 132 | connection.on_shoot_set(self, fire) 133 | 134 | return protocol, RapidConnection -------------------------------------------------------------------------------- /ratio.py: -------------------------------------------------------------------------------- 1 | """ 2 | K/D ratio script. 3 | 4 | Author: TheGrandmaster 5 | Maintainer: mat^2 6 | """ 7 | 8 | from commands import get_player, add 9 | from pyspades.constants import * 10 | 11 | # True if you want to include the headshot-death ratio in the ratio 12 | # NOTE: this makes the message overflow into two lines 13 | HEADSHOT_RATIO = False 14 | 15 | # List other types of kills as well 16 | EXTENDED_RATIO = False 17 | 18 | # "ratio" must be AFTER "votekick" in the config.txt script list 19 | RATIO_ON_VOTEKICK = True 20 | IRC_ONLY = False 21 | 22 | def ratio(connection, user=None): 23 | msg = "You have" 24 | if user != None: 25 | connection = get_player(connection.protocol, user) 26 | msg = "%s has" 27 | if connection not in connection.protocol.players: 28 | raise KeyError() 29 | msg %= connection.name 30 | if connection not in connection.protocol.players: 31 | raise KeyError() 32 | 33 | kills = connection.ratio_kills 34 | deaths = float(max(1,connection.ratio_deaths)) 35 | headshotkills = connection.ratio_headshotkills 36 | meleekills = connection.ratio_meleekills 37 | grenadekills = connection.ratio_grenadekills 38 | 39 | msg += " a kill-death ratio of %.2f" % (kills/deaths) 40 | if HEADSHOT_RATIO: 41 | msg += ", headshot-death ratio of %.2f" % (headshotkills/deaths) 42 | msg += " (%s kills, %s deaths" % (kills, connection.ratio_deaths) 43 | if EXTENDED_RATIO: 44 | msg += ", %s headshot, %s melee, %s grenade" % (headshotkills, meleekills, grenadekills) 45 | msg += ")." 46 | return msg 47 | 48 | add(ratio) 49 | 50 | def apply_script(protocol, connection, config): 51 | class RatioConnection(connection): 52 | ratio_kills = 0 53 | ratio_headshotkills = 0 54 | ratio_meleekills = 0 55 | ratio_grenadekills = 0 56 | ratio_deaths = 0 57 | 58 | def on_kill(self, killer, type, grenade): 59 | if killer is not None and self.team is not killer.team: 60 | if self != killer: 61 | killer.ratio_kills += 1 62 | killer.ratio_headshotkills += type == HEADSHOT_KILL 63 | killer.ratio_meleekills += type == MELEE_KILL 64 | killer.ratio_grenadekills += type == GRENADE_KILL 65 | 66 | self.ratio_deaths += 1 67 | return connection.on_kill(self, killer, type, grenade) 68 | 69 | class RatioProtocol(protocol): 70 | def on_votekick_start(self, instigator, victim, reason): 71 | result = protocol.on_votekick_start(self, instigator, victim, reason) 72 | if result is None and RATIO_ON_VOTEKICK: 73 | message = ratio(instigator, victim.name) 74 | if IRC_ONLY: 75 | self.irc_say('* ' + message) 76 | else: 77 | self.send_chat(message, irc = True) 78 | return result 79 | 80 | return RatioProtocol, RatioConnection -------------------------------------------------------------------------------- /reloadconfig.py: -------------------------------------------------------------------------------- 1 | from commands import add, admin 2 | import json 3 | 4 | @admin 5 | def reloadconfig(connection): 6 | new_config = {} 7 | try: 8 | new_config = json.load(open('config.txt', 'r')) 9 | if not isinstance(new_config, dict): 10 | raise ValueError('config.txt is not a mapping type') 11 | except ValueError, v: 12 | print 'Error reloading config:', v 13 | return 'Error reloading config. Check pyspades log for details.' 14 | connection.protocol.config.update(new_config) 15 | return 'Config reloaded!' 16 | 17 | add(reloadconfig) 18 | 19 | def apply_script(protocol, connection, config): 20 | return protocol, connection 21 | -------------------------------------------------------------------------------- /removesquad.py: -------------------------------------------------------------------------------- 1 | """ 2 | removesquad.py 3 | 4 | Removes a player from their current squad, if any. 5 | Useful in conjunction with the jail script. 6 | 7 | REQUIRES SQUAD.PY 8 | """ 9 | 10 | from commands import admin, name, get_player, add 11 | 12 | @admin 13 | @name('rs') 14 | def remove_squad(self, player_name): 15 | player = get_player(self.protocol, player_name) 16 | 17 | if player.squad is not None: 18 | player.squad = None 19 | player.squad_pref = None 20 | player.join_squad(None, None) 21 | return 'Removed player %s from their squad.' % player.name 22 | else: 23 | return 'Player %s is not in a squad!' % player.name 24 | 25 | add(remove_squad) 26 | 27 | def apply_script(protocol, connection, config): 28 | return protocol, connection 29 | -------------------------------------------------------------------------------- /rollback.py: -------------------------------------------------------------------------------- 1 | """ 2 | Progressively roll backs map to their original state (or to another map). 3 | 4 | Maintainer: hompy 5 | Refactored by infogulch 6 | """ 7 | 8 | from pyspades.vxl import VXLData 9 | from pyspades.contained import BlockAction, SetColor 10 | from pyspades.constants import * 11 | from pyspades.common import coordinates, make_color 12 | from map import Map, MapNotFound, check_rotation 13 | from commands import add, admin 14 | import cbc 15 | import time 16 | import operator 17 | 18 | S_INVALID_MAP_NAME = 'Invalid map name' 19 | S_ROLLBACK_IN_PROGRESS = 'Rollback in progress' 20 | S_ROLLBACK_COMMENCED = '{player} commenced a rollback...' 21 | S_AUTOMATIC_ROLLBACK_PLAYER_NAME = 'Map' 22 | S_NO_ROLLBACK_IN_PROGRESS = 'No rollback in progress' 23 | S_ROLLBACK_CANCELLED = 'Rollback cancelled by {player}' 24 | S_ROLLBACK_ENDED = 'Rollback ended. {result}' 25 | S_MAP_CHANGED = 'Map was changed' 26 | S_ROLLBACK_PROGRESS = 'Rollback progress {percent:.0%}' 27 | S_ROLLBACK_COLOR_PASS = 'Rollback color pass {percent:.0%}' 28 | S_ROLLBACK_TIME_TAKEN = 'Time taken: {seconds:.3}s' 29 | 30 | NON_SURFACE_COLOR = (0, 0, 0) 31 | 32 | @admin 33 | def rollmap(connection, mapname = None, value = None): 34 | start_x, start_y, end_x, end_y = 0, 0, 512, 512 35 | if value is not None: 36 | start_x, start_y = coordinates(value) 37 | end_x, end_y = start_x + 64, start_y + 64 38 | return connection.protocol.start_rollback(connection, mapname, 39 | start_x, start_y, end_x, end_y) 40 | 41 | @admin 42 | def rollback(connection, value = None): 43 | return rollmap(connection, value = value) 44 | 45 | @admin 46 | def rollbackcancel(connection): 47 | return connection.protocol.rollback_cancel(connection) 48 | 49 | for func in (rollmap, rollback, rollbackcancel): 50 | add(func) 51 | 52 | def apply_script(protocol, connection, config): 53 | protocol, connection = cbc.apply_script(protocol, connection, config) 54 | 55 | rollback_on_game_end = config.get('rollback_on_game_end', False) 56 | 57 | class RollbackConnection(connection): 58 | def on_block_destroy(self, x, y, z, value): 59 | if self.protocol.rollback_handle is not None: 60 | return False 61 | return connection.on_block_destroy(self, x, y, z, value) 62 | 63 | class RollbackProtocol(protocol): 64 | rollback_time_between_progress_updates = 10.0 65 | rollback_map = None 66 | rollback_handle = None 67 | 68 | def start_rollback(self, connection, mapname, start_x, start_y, 69 | end_x, end_y, ignore_indestructable = True): 70 | 71 | if self.rollback_handle is not None: 72 | return S_ROLLBACK_IN_PROGRESS 73 | if mapname is None: 74 | map = self.rollback_map 75 | else: 76 | try: 77 | maps = check_rotation([mapname]) 78 | if not maps: 79 | return S_INVALID_MAP_NAME 80 | map = Map(maps[0]).data 81 | except MapNotFound as error: 82 | return error.message 83 | name = (connection.name if connection is not None 84 | else S_AUTOMATIC_ROLLBACK_PLAYER_NAME) 85 | message = S_ROLLBACK_COMMENCED.format(player = name) 86 | self.send_chat(message, irc = True) 87 | generator = self.create_rollback_generator(self.map, 88 | map, start_x, start_y, end_x, end_y, ignore_indestructable) 89 | self.rollback_handle = self.cbc_add(generator 90 | , self.rollback_time_between_progress_updates 91 | , self.rollback_callback 92 | , connection) 93 | 94 | def rollback_cancel(self, connection): 95 | if self.rollback_handle is None: 96 | return S_NO_ROLLBACK_IN_PROGRESS 97 | self.cbc_cancel(self.rollback_handle) #should call rollback_callback automatically 98 | 99 | def rollback_callback(self, cbctype, progress, elapsed, connection): 100 | message = '' 101 | if cbctype == self.CBC_CANCELLED: 102 | message = S_ROLLBACK_CANCELLED.format(player = connection.name) 103 | self.rollback_handle = None 104 | elif cbctype == self.CBC_FINISHED: 105 | message = S_ROLLBACK_ENDED.format(result = '') + '\n' + S_ROLLBACK_TIME_TAKEN.format(seconds = elapsed) 106 | self.rollback_handle = None 107 | elif cbctype == self.CBC_UPDATE and progress >= 0.0: 108 | message = S_ROLLBACK_PROGRESS.format(percent = progress) 109 | elif cbctype == self.CBC_UPDATE and progress < 0.0: 110 | message = S_ROLLBACK_COLOR_PASS.format(percent = abs(progress)) 111 | if message != '': 112 | self.send_chat(message, irc = True) 113 | 114 | def create_rollback_generator(self, cur, new, start_x, start_y, 115 | end_x, end_y, ignore_indestructable): 116 | surface = {} 117 | block_action = BlockAction() 118 | block_action.player_id = 31 119 | set_color = SetColor() 120 | set_color.value = make_color(*NON_SURFACE_COLOR) 121 | set_color.player_id = 31 122 | self.send_contained(set_color, save = True) 123 | old = cur.copy() 124 | check_protected = hasattr(protocol, 'protected') 125 | x_count = abs(start_x - end_x) 126 | for x in xrange(start_x, end_x): 127 | block_action.x = x 128 | for y in xrange(start_y, end_y): 129 | block_action.y = y 130 | if check_protected and self.is_protected(x, y, 0): 131 | continue 132 | for z in xrange(63): 133 | action = None 134 | cur_solid = cur.get_solid(x, y, z) 135 | new_solid = new.get_solid(x, y, z) 136 | if cur_solid and not new_solid: 137 | if (not ignore_indestructable and 138 | self.is_indestructable(x, y, z)): 139 | continue 140 | else: 141 | action = DESTROY_BLOCK 142 | cur.remove_point(x, y, z) 143 | elif new_solid: 144 | new_is_surface = new.is_surface(x, y, z) 145 | if new_is_surface: 146 | new_color = new.get_color(x, y, z) 147 | if not cur_solid and new_is_surface: 148 | surface[(x, y, z)] = new_color 149 | elif not cur_solid and not new_is_surface: 150 | action = BUILD_BLOCK 151 | cur.set_point(x, y, z, NON_SURFACE_COLOR) 152 | elif cur_solid and new_is_surface: 153 | old_is_surface = old.is_surface(x, y, z) 154 | if old_is_surface: 155 | old_color = old.get_color(x, y, z) 156 | if not old_is_surface or old_color != new_color: 157 | surface[(x, y, z)] = new_color 158 | action = DESTROY_BLOCK 159 | cur.remove_point(x, y, z) 160 | if action is not None: 161 | block_action.z = z 162 | block_action.value = action 163 | self.send_contained(block_action, save = True) 164 | yield action is not None, ((x-start_x+0.0) / x_count) 165 | last_color = None 166 | block_action.value = BUILD_BLOCK 167 | i = 0.0 168 | for pos, color in sorted(surface.iteritems()): 169 | x, y, z = pos 170 | packets_sent = 0 171 | if color != last_color: 172 | set_color.value = make_color(*color) 173 | self.send_contained(set_color, save = True) 174 | packets_sent += 1 175 | last_color = color 176 | cur.set_point(x, y, z, color) 177 | block_action.x = x 178 | block_action.y = y 179 | block_action.z = z 180 | self.send_contained(block_action, save = True) 181 | packets_sent += 1 182 | i += 1 183 | yield packets_sent, -(i / len(surface)) 184 | 185 | def on_map_change(self, map): 186 | self.rollback_map = map.copy() 187 | protocol.on_map_change(self, map) 188 | 189 | def on_game_end(self): 190 | if self.rollback_on_game_end: 191 | self.start_rollback(None, None, 0, 0, 512, 512, False) 192 | protocol.on_game_end(self) 193 | 194 | return RollbackProtocol, RollbackConnection -------------------------------------------------------------------------------- /savemap.py: -------------------------------------------------------------------------------- 1 | """ 2 | Saves current map on shutdown (and optionally loads it again on startup) 3 | 4 | /savemap manually saves the map in the format of .manual.