diff --git a/contrib/scripts/arena.py b/contrib/scripts/arena.py
index ef112f9b..94e300e4 100644
--- a/contrib/scripts/arena.py
+++ b/contrib/scripts/arena.py
@@ -13,19 +13,27 @@
# Spawn locations and gate locations MUST be present in the map metadata (map txt file)
# for arena to work properly.
-# Spawn locations for the green team are set by using the data from the 'arena_green_spawn'
-# tuple in the extensions dictionary. Likewise, the blue spawn is set with the 'arena_blue_spawn'
-# key.
+# The spawn location/s for the green team are set by using the data from the 'arena_green_spawns'
+# tuple in the extensions dictionary. Likewise, the blue spawn/s is set with the 'arena_blue_spawns'
+# key. 'arena_green_spawns' and 'arena_blue_spawns' are tuples which contain tuples of spawn
+# coordinates. Spawn locations are chosen randomly.
+
+# NOTE THAT THE SCRIPT RETAINS BACKWARDS COMPATIBILITY with the old 'arena_green_spawn' and
+# 'arena_blue_spawn'
+
+# The 'arena_max_spawn_distance' can be used to set MAX_SPAWN_DISTANCE on a map by map
+# basis. See the comment by MAX_SPAWN_DISTANCE for more information
# The locations of gates is also determined in the map metadata. 'arena_gates' is a
# tuple of coordinates in the extension dictionary. Each gate needs only one block
# to be specified (since each gate is made of a uniform color)
# Sample extensions dictionary of an arena map with two gates:
+# In this example there is one spawn location for blue and two spawn locations for green.
# extensions = {
# 'arena': True,
-# 'arena_blue_spawn' : (128, 256, 60),
-# 'arena_green_spawn' : (384, 256, 60),
+# 'arena_blue_spawns' : ((128, 256, 60),),
+# 'arena_green_spawns' : ((384, 256, 60), (123, 423, 51)),
# 'arena_gates': ((192, 236, 59), (320, 245, 60))
# }
@@ -36,29 +44,40 @@
from twisted.internet import reactor
from twisted.internet.task import LoopingCall
from commands import add, admin
+import random
+import math
# If ALWAYS_ENABLED is False, then the 'arena' key must be set to True in
# the 'extensions' dictionary in the map metadata
ALWAYS_ENABLED = True
# How long should be spent between rounds in arena (seconds)
-SPAWN_ZONE_TIME = 20.0
+SPAWN_ZONE_TIME = 15.0
+
+# How many seconds a team color should be shown after they win a round
+# Set to 0 to disable this feature.
+TEAM_COLOR_TIME = 4.0
# Maximum duration that a round can last. Time is in seconds. Set to 0 to
# disable the time limit
MAX_ROUND_TIME = 180
-MAP_CHANGE_DELAY = 30.0
+MAP_CHANGE_DELAY = 25.0
# Coordinates to hide the tent and the intel
HIDE_COORD = (0, 0, 63)
+# Max distance a player can be from a spawn while the players are held within
+# the gates. If they get outside this they are teleported to a spawn.
+# Used to teleport players who glitch through the map back into the spawns.
+MAX_SPAWN_DISTANCE = 15.0
+
BUILDING_ENABLED = False
if MAX_ROUND_TIME >= 60:
MAX_ROUND_TIME_TEXT = '%.2f minutes' % (float(MAX_ROUND_TIME)/60.0)
else:
- MAX_ROUND_TIME_TEXT = MAX_ROUND_TIME + ' seconds'
+ MAX_ROUND_TIME_TEXT = str(MAX_ROUND_TIME) + ' seconds'
@admin
def coord(connection):
@@ -259,12 +278,30 @@ def on_team_join(self, team):
self.world_object.dead = True
self.protocol.check_round_end()
return returned
+
+ def on_position_update(self):
+ if not self.protocol.arena_running:
+ min_distance = None
+ pos = self.world_object.position
+ for spawn in self.team.arena_spawns:
+ xd = spawn[0] - pos.x
+ yd = spawn[1] - pos.y
+ zd = spawn[2] - pos.z
+ distance = math.sqrt(xd ** 2 + yd ** 2 + zd ** 2)
+ if min_distance is None or distance < min_distance:
+ min_distance = distance
+ if min_distance > self.protocol.arena_max_spawn_distance:
+ self.set_location(random.choice(self.team.arena_spawns))
+ self.refill()
+ return connection.on_position_update(self)
def get_respawn_time(self):
- if self.protocol.arena_running:
- return -1
- else:
- return 1
+ if self.protocol.arena_enabled:
+ if self.protocol.arena_running:
+ return -1
+ else:
+ return 1
+ return connection.get_respawn_time(self);
def respawn(self):
if self.protocol.arena_running:
@@ -279,7 +316,7 @@ def on_spawn(self, pos):
def on_spawn_location(self, pos):
if self.protocol.arena_enabled:
- return self.team.arena_spawn
+ return random.choice(self.team.arena_spawns)
return connection.on_spawn_location(self, pos)
def on_flag_take(self):
@@ -304,6 +341,8 @@ class ArenaProtocol(protocol):
arena_take_flag = False
arena_countdown_timers = None
arena_limit_timer = None
+ arena_old_fog_color = None
+ arena_max_spawn_distance = MAX_SPAWN_DISTANCE
def check_round_end(self, killer = None, message = True):
if not self.arena_running:
@@ -332,10 +371,15 @@ def arena_time_limit(self):
def arena_win(self, team, killer = None):
if not self.arena_running:
return
+ if self.arena_old_fog_color is None and TEAM_COLOR_TIME > 0:
+ self.arena_old_fog_color = self.fog_color
+ self.set_fog_color(team.color)
+ reactor.callLater(TEAM_COLOR_TIME, self.arena_reset_fog_color)
if killer is None or killer.team is not team:
for player in team.get_players():
- killer = player
- break
+ if not player.world_object.dead:
+ killer = player
+ break
if killer is not None:
self.arena_take_flag = True
killer.take_flag()
@@ -343,6 +387,13 @@ def arena_win(self, team, killer = None):
self.send_chat(team.name + ' team wins the round!')
self.begin_arena_countdown()
+ def arena_reset_fog_color(self):
+ if self.arena_old_fog_color is not None:
+ # Shitty fix for disco on game end
+ self.old_fog_color = self.arena_old_fog_color
+ self.set_fog_color(self.arena_old_fog_color)
+ self.arena_old_fog_color = None
+
def arena_remaining_message(self):
if not self.arena_running:
return
@@ -365,6 +416,7 @@ def on_map_change(self, map):
self.arena_enabled = extensions['arena']
else:
self.arena_enabled = False
+ self.arena_max_spawn_distance = MAX_SPAWN_DISTANCE
if self.arena_enabled:
self.old_respawn_time = self.respawn_time
self.respawn_time = 0
@@ -374,14 +426,20 @@ def on_map_change(self, map):
if extensions.has_key('arena_gates'):
for gate in extensions['arena_gates']:
self.gates.append(Gate(*gate, protocol_obj=self))
- if extensions.has_key('arena_green_spawn'):
- self.green_team.arena_spawn = extensions['arena_green_spawn']
+ if extensions.has_key('arena_green_spawns'):
+ self.green_team.arena_spawns = extensions['arena_green_spawns']
+ elif extensions.has_key('arena_green_spawn'):
+ self.green_team.arena_spawns = (extensions['arena_green_spawn'],)
else:
- raise CustomException('No arena_green_spawn given in map metadata.')
- if extensions.has_key('arena_blue_spawn'):
- self.blue_team.arena_spawn = extensions['arena_blue_spawn']
+ raise CustomException('No arena_green_spawns given in map metadata.')
+ if extensions.has_key('arena_blue_spawns'):
+ self.blue_team.arena_spawns = extensions['arena_blue_spawns']
+ elif extensions.has_key('arena_blue_spawn'):
+ self.blue_team.arena_spawns = (extensions['arena_blue_spawn'],)
else:
- raise CustomException('No arena_blue_spawn given in map metadata.')
+ raise CustomException('No arena_blue_spawns given in map metadata.')
+ if extensions.has_key('arena_max_spawn_distance'):
+ self.arena_max_spawn_distance = extensions['arena_max_spawn_distance']
self.delay_arena_countdown(MAP_CHANGE_DELAY)
self.begin_arena_countdown()
else:
@@ -396,6 +454,7 @@ def on_map_change(self, map):
self.arena_running = False
self.arena_counting_down = False
self.arena_limit_timer = None
+ self.arena_old_fog_color = None
self.old_respawn_time = None
self.old_building = None
self.old_killing = None
@@ -414,9 +473,9 @@ def arena_spawn(self):
if player.team.spectator:
continue
if player.world_object.dead:
- player.spawn(player.team.arena_spawn)
+ player.spawn(random.choice(player.team.arena_spawns))
else:
- player.set_location(player.team.arena_spawn)
+ player.set_location(random.choice(player.team.arena_spawns))
player.refill()
def begin_arena_countdown(self):
diff --git a/feature_server/commands.py b/feature_server/commands.py
index 906829ef..a06bd795 100644
--- a/feature_server/commands.py
+++ b/feature_server/commands.py
@@ -16,14 +16,18 @@
# along with pyspades. If not, see .
import math
+import inspect
from random import choice
from pyspades.constants import *
from pyspades.common import prettify_timespan
from pyspades.server import parse_command
from twisted.internet import reactor
-
from map import check_rotation
+commands = {}
+aliases = {}
+rights = {}
+
class InvalidPlayer(Exception):
pass
@@ -33,15 +37,26 @@ class InvalidSpectator(InvalidPlayer):
class InvalidTeam(Exception):
pass
-def restrict(func, user_types):
+def add_rights(func_name, *user_types):
+ for user_type in user_types:
+ if user_type in rights:
+ rights[user_type].add(func_name)
+ else:
+ rights[user_type] = set([func_name])
+
+def restrict(func, *user_types):
def new_func(connection, *arg, **kw):
return func(connection, *arg, **kw)
new_func.func_name = func.func_name
- new_func.user_types = set(user_types)
+ new_func.user_types = user_types
+ new_func.argspec = inspect.getargspec(func)
return new_func
+def has_rights(f, connection):
+ return not hasattr(f, 'user_types') or f.func_name in connection.rights
+
def admin(func):
- return restrict(func, ('admin',))
+ return restrict(func, 'admin')
def name(name):
def dec(func):
@@ -90,6 +105,8 @@ def get_team(connection, value):
return connection.protocol.blue_team
elif value == 'green':
return connection.protocol.green_team
+ elif value == 'spectator':
+ return connection.protocol.spectator_team
raise InvalidTeam()
def join_arguments(arg, default = None):
@@ -194,12 +211,18 @@ def say(connection, *arg):
connection.protocol.send_chat(value)
connection.protocol.irc_say(value)
-@admin
-def kill(connection, value):
- player = get_player(connection.protocol, value, False)
+add_rights('kill', 'admin')
+def kill(connection, value = None):
+ if value is None:
+ player = connection
+ else:
+ if not connection.rights.kill:
+ return "You can't use this command"
+ player = get_player(connection.protocol, value, False)
player.kill()
- message = '%s killed %s' % (connection.name, player.name)
- connection.protocol.send_chat(message, irc = True)
+ if connection is not player:
+ message = '%s killed %s' % (connection.name, player.name)
+ connection.protocol.send_chat(message, irc = True)
@admin
def heal(connection, player = None):
@@ -301,7 +324,7 @@ def unlock(connection, value):
team.name))
@admin
-def switch(connection, player = None):
+def switch(connection, player = None, team = None):
protocol = connection.protocol
if player is not None:
player = get_player(protocol, player)
@@ -312,9 +335,13 @@ def switch(connection, player = None):
if player.team.spectator:
player.send_chat("The switch command can't be used on a spectating player.")
return
+ if team is None:
+ new_team = player.team.other
+ else:
+ new_team = get_team(connection, team)
if player.invisible:
old_team = player.team
- player.team = player.team.other
+ player.team = new_team
player.on_team_changed(old_team)
player.spawn(player.world_object.position.get())
player.send_chat('Switched to %s team' % player.team.name)
@@ -324,7 +351,7 @@ def switch(connection, player = None):
protocol.irc_say('* %s silently switched teams' % player.name)
else:
player.respawn_time = protocol.respawn_time
- player.set_team(player.team.other)
+ player.set_team(new_team)
protocol.send_chat('%s switched teams' % player.name, irc = True)
@name('setbalance')
@@ -450,7 +477,8 @@ def teleport(connection, player1, player2 = None, silent = False):
silent = silent or player.invisible
message = '%s ' + ('silently ' if silent else '') + 'teleported to %s'
message = message % (player.name, target.name)
- player.set_location(target.get_location())
+ x, y, z = target.get_location()
+ player.set_location(((x-0.5), (y-0.5), (z+0.5)))
if silent:
connection.protocol.irc_say('* ' + message)
else:
@@ -480,6 +508,13 @@ def go_to(connection, value):
raise KeyError()
move(connection, connection.name, value, silent = connection.invisible)
+@name('gotos')
+@admin
+def go_to_silent(connection, value):
+ if connection not in connection.protocol.players:
+ raise KeyError()
+ move(connection, connection.name, value, True)
+
@admin
def move(connection, player, value, silent = False):
player = get_player(connection.protocol, player)
@@ -510,8 +545,9 @@ def where(connection, value = None):
return '%s is in %s (%s, %s, %s)' % (connection.name,
to_coordinates(x, y), int(x), int(y), int(z))
+@alias('gods')
@admin
-def god(connection, value = None):
+def godsilent(connection, value = None):
if value is not None:
connection = get_player(connection.protocol, value)
elif connection not in connection.protocol.players:
@@ -521,6 +557,11 @@ def god(connection, value = None):
connection.god_build = connection.god
else:
connection.god_build = False
+ return 'You have entered god mode.'
+
+@admin
+def god(connection, value = None):
+ godsilent(connection, value)
if connection.god:
message = '%s entered GOD MODE!' % connection.name
else:
@@ -874,10 +915,12 @@ def weapon(connection, value):
teleport,
tpsilent,
go_to,
+ go_to_silent,
move,
unstick,
where,
god,
+ godsilent,
god_build,
fly,
invisible,
@@ -901,10 +944,6 @@ def weapon(connection, value):
mapname
]
-commands = {}
-aliases = {}
-rights = {}
-
def add(func, name = None):
"""
Function to add a command from scripts
@@ -912,13 +951,9 @@ def add(func, name = None):
if name is None:
name = func.func_name
name = name.lower()
- user_types = getattr(func, 'user_types', None)
- if user_types is not None:
- for user_type in user_types:
- if user_type in rights:
- rights[user_type].add(name)
- else:
- rights[user_type] = set([name])
+ if not hasattr(func, 'argspec'):
+ func.argspec = inspect.getargspec(func)
+ add_rights(name, *getattr(func, 'user_types', ()))
commands[name] = func
try:
for alias in func.aliases:
@@ -974,15 +1009,23 @@ def handle_command(connection, command, parameters):
command_func = commands[command]
except KeyError:
return # 'Invalid command'
+ aspec = command_func.argspec
+ min_params = len(aspec.args) - 1 - len(aspec.defaults or ())
+ max_params = len(aspec.args) - 1 if aspec.varargs is None else None
+ len_params = len(parameters)
+ if (len_params < min_params
+ or max_params is not None and len_params > max_params):
+ return 'Invalid number of arguments for %s' % command
try:
- if (hasattr(command_func, 'user_types') and
- command_func.func_name not in connection.rights):
- return "You can't use this command"
+ if not has_rights(command_func, connection):
+ return "You can't use this command"
return command_func(connection, *parameters)
except KeyError:
return # 'Invalid command'
- except TypeError:
- return 'Invalid number of arguments for %s' % command
+ except TypeError as t:
+ print 'Command', command, 'failed with args:', parameters
+ print t
+ return 'Command failed'
except InvalidPlayer:
return 'No such player'
except InvalidTeam:
@@ -990,26 +1033,6 @@ def handle_command(connection, command, parameters):
except ValueError:
return 'Invalid parameters'
-def debug_handle_command(connection, command, parameters):
- # use this when regular handle_command eats errors
- if connection in connection.protocol.players:
- connection.send_chat("Commands are in DEBUG mode")
- command = command.lower()
- try:
- command = aliases[command]
- except KeyError:
- pass
- try:
- command_func = commands[command]
- except KeyError:
- return # 'Invalid command'
- if (hasattr(command_func, 'user_types') and
- command_func.func_name not in connection.rights):
- return "You can't use this command"
- return command_func(connection, *parameters)
-
-# handle_command = debug_handle_command
-
def handle_input(connection, input):
# for IRC and console
return handle_command(connection, *parse_command(input))
diff --git a/feature_server/config.txt.default b/feature_server/config.txt.default
index 795d30d7..252df8aa 100644
--- a/feature_server/config.txt.default
+++ b/feature_server/config.txt.default
@@ -142,4 +142,4 @@
"load_saved_map" : false,
"rollback_on_game_end" : false,
"afk_time_limit" : 30
-}
\ No newline at end of file
+}
diff --git a/feature_server/irc.py b/feature_server/irc.py
index e3dc217f..85ea5d7f 100644
--- a/feature_server/irc.py
+++ b/feature_server/irc.py
@@ -237,7 +237,7 @@ def format_name_color(player):
'%s #%s' % (player.name, player.player_id))
def irc(func):
- return commands.restrict(func, ('irc',))
+ return commands.restrict(func, 'irc')
@irc
def who(connection):
diff --git a/feature_server/map.py b/feature_server/map.py
index 33adb9d3..b8e1b3be 100644
--- a/feature_server/map.py
+++ b/feature_server/map.py
@@ -49,10 +49,11 @@ def __init__(self, rot_info, load_dir = DEFAULT_LOAD_DIR):
self.load_information(rot_info, load_dir)
if self.gen_script:
- self.name = '%s #%s' % (rot_info.name, rot_info.get_seed())
+ seed = rot_info.get_seed()
+ self.name = '%s #%s' % (rot_info.name, seed)
print "Generating map '%s'..." % self.name
- random.seed(rot_info.get_seed())
- self.data = self.gen_script(rot_info.name, rot_info.get_seed())
+ random.seed(seed)
+ self.data = self.gen_script(rot_info.name, seed)
else:
print "Loading map '%s'..." % self.name
self.load_vxl(rot_info, load_dir)
@@ -112,10 +113,10 @@ def __init__(self, name = "pyspades"):
self.name = name
def get_seed(self):
- if self.seed is None:
- random.seed()
- self.seed = random.randint(0, math.pow(2, 31))
- return self.seed
+ if self.seed is not None:
+ return self.seed
+ random.seed()
+ return random.randint(0, math.pow(2, 31))
def get_map_filename(self, load_dir = DEFAULT_LOAD_DIR):
return os.path.join(load_dir, '%s.vxl' % self.name)
diff --git a/feature_server/run.py b/feature_server/run.py
index 8c1976dd..c0acd2f6 100644
--- a/feature_server/run.py
+++ b/feature_server/run.py
@@ -369,13 +369,17 @@ def on_team_join(self, team):
return False
if team.locked:
self.send_chat('Team is locked')
+ if not team.spectator and not team.other.locked:
+ return team.other
return False
balanced_teams = self.protocol.balanced_teams
if balanced_teams and not team.spectator:
other_team = team.other
if other_team.count() < team.count() + 1 - balanced_teams:
- self.send_chat('Team is full')
- return False
+ if other_team.locked:
+ return False
+ self.send_chat('Team is full, moved to %s' % other_team.name)
+ return other_team
self.last_switch = reactor.seconds()
def on_chat(self, value, global_message):
@@ -668,7 +672,10 @@ def __init__(self, interface, config):
print 'REMEMBER TO CHANGE THE DEFAULT ADMINISTRATOR PASSWORD!'
elif not password:
self.everyone_is_admin = True
- commands.rights.update(config.get('rights', {}))
+
+ for user_type, func_names in config.get('rights', {}).iteritems():
+ for func_name in func_names:
+ commands.add_rights(func_name, user_type)
port = self.port = config.get('port', 32887)
ServerProtocol.__init__(self, port, interface)
diff --git a/feature_server/scripts/grownade.py b/feature_server/scripts/grownade.py
index 2b20d496..c5ee3b0d 100644
--- a/feature_server/scripts/grownade.py
+++ b/feature_server/scripts/grownade.py
@@ -68,6 +68,31 @@
S_SPECIFY_FILES = 'Specify model files to load. Wildcards are allowed, ' \
'e.g.: "bunker", "tree*"'
+class ModelLoadFailure(Exception):
+ pass
+
+def load_models(expression):
+ if not os.path.isdir(KV6_DIR):
+ raise ModelLoadFailure(S_NO_KV6_FOLDER)
+ if not os.path.splitext(expression)[-1]:
+ # append default extension
+ expression += '.kv6'
+ paths = glob(os.path.join(KV6_DIR, expression))
+ if not paths:
+ raise ModelLoadFailure(S_NO_MATCHES.format(expression = expression))
+
+ # attempt to load models, discard invalid ones
+ models, loaded, failed = [], [], []
+ for path in paths:
+ model = KV6Model(path)
+ filename = os.path.split(path)[-1]
+ if model.voxels:
+ models.append(model)
+ loaded.append(filename)
+ else:
+ failed.append(filename)
+ return models, loaded, failed
+
@name('model')
@admin
def model_grenades(connection, expression = None):
@@ -78,35 +103,20 @@ def model_grenades(connection, expression = None):
result = None
if expression:
- if not os.path.isdir(KV6_DIR):
- return S_NO_KV6_FOLDER
- if not os.path.splitext(expression)[-1]:
- # append default extension
- expression += '.kv6'
- paths = glob(os.path.join(KV6_DIR, expression))
- if not paths:
- return S_NO_MATCHES.format(expression = expression)
-
- # attempt to load models, discard invalid ones
- models, loaded, failed = [], [], []
- for path in paths:
- model = KV6Model(path)
- filename = os.path.split(path)[-1]
- if model.voxels:
- models.append(model)
- loaded.append(filename)
- else:
- failed.append(filename)
- if len(loaded) == 1:
- result = S_LOADED_SINGLE.format(filename = loaded[0])
- elif len(loaded) > 1:
- result = S_LOADED.format(filenames = ', '.join(loaded))
- if failed:
- player.send_chat(S_FAILED.format(filenames = ', '.join(failed)))
- player.send_chat(S_PIVOT_TIP)
-
- if models:
- player.grenade_models = models
+ try:
+ models, loaded, failed = load_models(expression)
+ except ModelLoadFailure as err:
+ result = str(err)
+ else:
+ if len(loaded) == 1:
+ result = S_LOADED_SINGLE.format(filename = loaded[0])
+ elif len(loaded) > 1:
+ result = S_LOADED.format(filenames = ', '.join(loaded))
+ if failed:
+ player.send_chat(S_FAILED.format(filenames = ', '.join(failed)))
+ player.send_chat(S_PIVOT_TIP)
+ if models:
+ player.grenade_models = models
elif player.grenade_models:
player.grenade_models = None
result = S_CANCEL
diff --git a/feature_server/scripts/platform.py b/feature_server/scripts/platform.py
index 57c69324..2f85ad29 100644
--- a/feature_server/scripts/platform.py
+++ b/feature_server/scripts/platform.py
@@ -64,10 +64,10 @@
See /trigger for more information on who the "activating players" are.
action:
- height [speed=0.25] [delay]
- raise [speed=0.25] [delay]
- lower [speed=0.25] [delay]
- elevator [speed=0.75] [delay] [wait=3.0]
+ height [speed=0.15] [delay]
+ raise [speed=0.15] [delay]
+ lower [speed=0.15] [delay]
+ elevator [speed=0.25] [delay] [wait=3.0]
Speed determines how fast the platform moves, in seconds.
Delay is the amount of time spent waiting before the platform
actually starts to move.
@@ -161,6 +161,12 @@
Saves all platform and button data to mapname_platform.txt
Use SAVE_ON_MAP_CHANGE and AUTOSAVE_EVERY to avoid having to manually save.
+/reach
+ Increases your button activation reach. Meant to be used for getting rid
+ of distance triggering buttons from a safe distance.
+
+ Use again to revert to normal.
+
Maintainer: hompy
"""
@@ -171,7 +177,6 @@
# Prevent platforms from being hidden forever
# Negative heights
# Nicer way of having invisible buttons?
-# 'invisibility' mode to get in range of annoying distance buttons
# Platforms crushing players
# Stop platform action?
@@ -192,12 +197,14 @@
from commands import add, admin, name, alias, join_arguments
from map import DEFAULT_LOAD_DIR
-SAVE_ON_MAP_CHANGE = True
+SAVE_ON_MAP_CHANGE = False
AUTOSAVE_EVERY = 0.0 # minutes, 0 = disabled
MAX_DISTANCE = 64.0
MIN_COOLDOWN = 0.1 # seconds
S_SAVED = 'Platforms saved'
+S_REACH = 'You can now reach to buttons from from far away'
+S_NO_REACH = 'Button reach reverted to normal'
S_EXIT_BLOCKING_STATE = "You must first leave {state} mode!"
S_NOT_WORKING = 'This button is disabled'
S_COMMAND_CANCEL = "Aborting {command} command"
@@ -239,10 +246,10 @@
S_ACTION_USAGE = 'Usage: /action <{commands}>'
S_ACTION_ADD_USAGE = 'Usage: /action add <{actions}>'
S_ACTION_DELETE_USAGE = 'Usage: /action del <#|all>'
-S_ACTION_HEIGHT_USAGE = 'Usage: /action add height [speed=0.25] [delay]'
-S_ACTION_RAISE_USAGE = 'Usage: /action add raise [speed=0.25] [delay]'
-S_ACTION_LOWER_USAGE = 'Usage: /action add lower [speed=0.25] [delay]'
-S_ACTION_ELEVATOR_USAGE = 'Usage: /action add elevator [speed=0.75] ' \
+S_ACTION_HEIGHT_USAGE = 'Usage: /action add height [speed=0.15] [delay]'
+S_ACTION_RAISE_USAGE = 'Usage: /action add raise [speed=0.15] [delay]'
+S_ACTION_LOWER_USAGE = 'Usage: /action add lower [speed=0.15] [delay]'
+S_ACTION_ELEVATOR_USAGE = 'Usage: /action add elevator [speed=0.25] ' \
'[delay] [wait=3.0]'
S_ACTION_OUTPUT_USAGE = 'Usage: /action add output [delay]'
S_ACTION_TELEPORT_USAGE = 'Usage: /action add teleport '
@@ -326,6 +333,7 @@
}
ACTION_RAY_LENGTH = 8.0
+ACTION_RAY_LENGTH_LONG = 32.0
ACTION_COOLDOWN = 0.25
def parseargs(signature, args):
@@ -356,6 +364,14 @@ def save(connection):
connection.protocol.dump_platform_json()
return S_SAVED
+@admin
+def reach(connection):
+ if connection not in connection.protocol.players:
+ raise ValueError()
+ long = connection.reach == ACTION_RAY_LENGTH_LONG
+ connection.reach = ACTION_RAY_LENGTH if long else ACTION_RAY_LENGTH_LONG
+ return S_REACH if not long else S_NO_REACH
+
@alias('p')
@name('platform')
@admin
@@ -532,12 +548,12 @@ def action_command(connection, *args):
if action == 'elevator':
signature = 'int [float float float]'
value, speed, delay, wait = parseargs(signature, args[2:])
- speed = 0.75 if speed is None else speed
+ speed = 0.25 if speed is None else speed
kwargs['wait'] = 3.0 if wait is None else wait
else:
signature = 'int [float float]'
value, speed, delay = parseargs(signature, args[2:])
- speed = 0.25 if speed is None else speed
+ speed = 0.15 if speed is None else speed
kwargs['mode'] = action
kwargs['height'] = value
kwargs['speed'] = speed
@@ -692,7 +708,7 @@ def trigger_command(connection, *args):
return usage
for func in (platform_command, button_command, action_command,
- trigger_command, save):
+ trigger_command, save, reach):
add(func)
def aabb(x, y, z, x1, y1, z1, x2, y2, z2):
@@ -1097,6 +1113,7 @@ class Platform(BaseObject):
frozen = False
mode = None
busy = False
+ running = False
delay_call = None
bound_triggers = None
@@ -1107,7 +1124,6 @@ def __init__(self, protocol, id, x1, y1, z1, x2, y2, z2, color):
self.z, self.start_z = z1, z2
self.height = self.start_z - self.z
self.color = color
- self.cycle_loop = LoopingCall(self.cycle)
for x, y, z in prism(x1, y1, z1, x2, y2, z2):
protocol.map.set_point(x, y, z, color)
@@ -1135,9 +1151,6 @@ def release(self):
if self.delay_call and self.delay_call.active():
self.delay_call.cancel()
self.delay_call = None
- if self.cycle_loop and self.cycle_loop.running:
- self.cycle_loop.stop()
- self.cycle_loop = None
def start(self, height, mode, speed, delay, wait = None, force = False):
if self.busy and not force:
@@ -1154,19 +1167,23 @@ def start(self, height, mode, speed, delay, wait = None, force = False):
if self.z == self.target_z:
return
self.busy = True
+ self.ticks_per_cycle = int(speed / UPDATE_FREQUENCY)
+ self.ticks_left = self.ticks_per_cycle
self.start_cycle_later(delay)
def start_cycle_later(self, delay):
+ self.running = False
if self.delay_call and self.delay_call.active():
self.delay_call.cancel()
self.delay_call = None
- if self.cycle_loop and self.cycle_loop.running:
- self.cycle_loop.stop()
- self.cycle_loop = LoopingCall(self.cycle)
if delay > 0.0:
- self.delay_call = callLater(delay, self.cycle_loop.start, self.speed)
+ self.delay_call = callLater(delay, self.run)
else:
- self.cycle_loop.start(self.speed)
+ self.run()
+
+ def run(self):
+ self.running = True
+ self.protocol.running_platforms.add(self)
def cycle(self):
if self.frozen:
@@ -1178,6 +1195,8 @@ def cycle(self):
# unstuck players
for player in self.protocol.players.itervalues():
obj = player.world_object
+ if obj is None:
+ continue
looking_up = obj.orientation.get()[2] < 0.4 # 0.5 (allow lag)
x, y, z = obj.position.get()
if aabb(x, y, z, self.x1, self.y1, self.z - 2,
@@ -1199,14 +1218,13 @@ def cycle(self):
self.z += 1
self.height = self.start_z - self.z
if self.z == self.target_z:
- self.cycle_loop.stop()
- self.cycle_loop = None
if self.mode == 'elevator':
self.mode = 'return'
self.target_z = self.last_z
self.start_cycle_later(self.wait)
else:
- self.busy = False
+ self.busy = self.running = False
+ self.protocol.running_platforms.discard(self)
if self.bound_triggers:
for trigger in self.bound_triggers:
trigger.callback(self)
@@ -1267,7 +1285,7 @@ def player_action(player, select, inspect):
if last_action is not None and seconds() - last_action <= ACTION_COOLDOWN:
return
player.last_action = seconds()
- location = player.world_object.cast_ray(ACTION_RAY_LENGTH)
+ location = player.world_object.cast_ray(player.reach)
if location is None:
return
x, y, z = location
@@ -1696,6 +1714,7 @@ class PlatformConnection(connection):
last_action = None
previous_button = None
previous_platform = None
+ reach = ACTION_RAY_LENGTH
def on_reset(self):
if self.states:
@@ -1788,6 +1807,7 @@ class PlatformProtocol(protocol):
highest_id = None
platforms = None
platform_json_dirty = False
+ running_platforms = None
buttons = None
position_triggers = None
autosave_loop = None
@@ -1795,6 +1815,7 @@ class PlatformProtocol(protocol):
def on_map_change(self, map):
self.highest_id = -1
self.platforms = {}
+ self.running_platforms = set()
self.buttons = MultikeyDict()
self.position_triggers = []
self.platform_json_dirty = False
@@ -1815,6 +1836,7 @@ def on_map_leave(self):
for button in self.buttons.itervalues():
button.release()
self.platforms = None
+ self.running_platforms = None
self.buttons = None
self.position_triggers = None
protocol.on_map_leave(self)
@@ -1823,6 +1845,11 @@ def on_world_update(self):
for player in self.players.itervalues():
for trigger in self.position_triggers:
trigger.callback(player)
+ for platform in list(self.running_platforms):
+ platform.ticks_left -= 1
+ if platform.ticks_left <= 0:
+ platform.ticks_left = platform.ticks_per_cycle
+ platform.cycle()
protocol.on_world_update(self)
def get_platform_json_path(self):
diff --git a/feature_server/scripts/squad.py b/feature_server/scripts/squad.py
index 50bea5b2..c65b3e60 100644
--- a/feature_server/scripts/squad.py
+++ b/feature_server/scripts/squad.py
@@ -221,7 +221,8 @@ def on_spawn(self, pos):
all_members = ([n for n in self.get_squad(self.team,
self.squad)['players'] if
n is not self])
- live_members = [n for n in all_members if n.hp]
+ live_members = [n for n in all_members if n.hp and not n.invisible and
+ not n.god]
membernames = [m.name for m in all_members]
memberstr = ""
for n in xrange(len(all_members)):
@@ -243,7 +244,8 @@ def on_spawn(self, pos):
self.send_chat('You are in squad %s, all alone.' %
self.squad)
if (self.squad_pref is not None and self.squad_pref.hp and
- self.squad_pref.team is self.team):
+ self.squad_pref.team is self.team and not self.squad_pref.invisible
+ and not self.squad_pref.god):
self.set_location_safe(self.get_follow_location(
self.squad_pref))
else:
diff --git a/pyspades/master.py b/pyspades/master.py
index c7b86957..9dea4384 100644
--- a/pyspades/master.py
+++ b/pyspades/master.py
@@ -103,6 +103,10 @@ def on_disconnect(self):
from pyspades.web import getPage
IP_GETTER = 'http://services.buildandshoot.com/getip'
+# other tools:
+# http://www.domaintools.com/research/my-ip/myip.xml
+# http://checkip.dyndns.com/
+# http://icanhazip.com/
def get_external_ip(interface = ''):
return getPage(IP_GETTER, bindAddress = (interface, 0))
@@ -112,4 +116,4 @@ def get_master_connection(protocol):
connection = protocol.connect(MasterConnection, HOST, PORT, MASTER_VERSION)
connection.server_protocol = protocol
connection.defer = defer
- return defer
\ No newline at end of file
+ return defer
diff --git a/pyspades/server.py b/pyspades/server.py
index 322def0f..12d3d25a 100644
--- a/pyspades/server.py
+++ b/pyspades/server.py
@@ -206,6 +206,7 @@ def __init__(self, *arg, **kw):
BaseConnection.__init__(self, *arg, **kw)
protocol = self.protocol
address = self.peer.address
+ self.total_blocks_removed = 0
self.address = (address.host, address.port)
self.respawn_time = protocol.respawn_time
self.rapids = SlidingWindow(RAPID_WINDOW_ENTRIES)
@@ -235,9 +236,13 @@ def loader_received(self, loader):
loaders.ShortPlayerData.id):
old_team = self.team
team = self.protocol.teams[contained.team]
- if self.on_team_join(team) == False:
- if not team.spectator:
- team = team.other
+
+ ret = self.on_team_join(team)
+ if ret is False:
+ team = self.protocol.spectator_team
+ elif ret is not None:
+ team = ret
+
self.team = team
if self.name is None:
name = contained.name
@@ -526,16 +531,17 @@ def loader_received(self, loader):
if self.on_block_destroy(x, y, z, value) == False:
return
elif value == DESTROY_BLOCK:
- if map.destroy_point(x, y, z):
+ count = map.destroy_point(x, y, z)
+ if count:
+ self.total_blocks_removed += count
self.blocks = min(50, self.blocks + 1)
self.on_block_removed(x, y, z)
elif value == SPADE_DESTROY:
- if map.destroy_point(x, y, z):
- self.on_block_removed(x, y, z)
- if map.destroy_point(x, y, z + 1):
- self.on_block_removed(x, y, z + 1)
- if map.destroy_point(x, y, z - 1):
- self.on_block_removed(x, y, z - 1)
+ for xyz in ((x, y, z), (x, y, z + 1), (x, y, z - 1)):
+ count = map.destroy_point(*xyz)
+ if count:
+ self.total_blocks_removed += count
+ self.on_block_removed(*xyz)
self.last_block_destroy = reactor.seconds()
block_action.x = x
block_action.y = y
@@ -602,8 +608,10 @@ def loader_received(self, loader):
self.set_weapon(self.weapon)
elif contained.id == loaders.ChangeTeam.id:
team = self.protocol.teams[contained.team]
- if self.on_team_join(team) == False:
+ ret = self.on_team_join(team)
+ if ret is False:
return
+ team = ret or team
self.set_team(team)
def is_valid_position(self, x, y, z, distance = None):
@@ -623,7 +631,7 @@ def check_refill(self):
self.last_refill = reactor.seconds()
if self.on_refill() != False:
self.refill()
-
+
def get_location(self):
position = self.world_object.position
return position.x, position.y, position.z
@@ -659,7 +667,7 @@ def set_location_safe(self, location, center = True):
y = y + pos_table[modpos][1]
z = z + pos_table[modpos][2]
self.set_location((x, y, z))
-
+
def set_location(self, location = None):
if location is None:
# used for rubberbanding
@@ -1007,9 +1015,9 @@ def grenade_exploded(self, grenade):
z = position.z
if x < 0 or x > 512 or y < 0 or y > 512 or z < 0 or z > 63:
return
- x = int(x)
- y = int(y)
- z = int(z)
+ x = int(math.floor(x))
+ y = int(math.floor(y))
+ z = int(math.floor(z))
for player_list in (self.team.other.get_players(), (self,)):
for player in player_list:
if not player.hp:
@@ -1031,7 +1039,9 @@ def grenade_exploded(self, grenade):
for nade_x in xrange(x - 1, x + 2):
for nade_y in xrange(y - 1, y + 2):
for nade_z in xrange(z - 1, z + 2):
- if map.destroy_point(nade_x, nade_y, nade_z):
+ count = map.destroy_point(nade_x, nade_y, nade_z)
+ if count:
+ self.total_blocks_removed += count
self.on_block_removed(nade_x, nade_y, nade_z)
block_action.x = x
block_action.y = y
diff --git a/pyspades/vxl.pxd b/pyspades/vxl.pxd
index 09f6458e..3620053b 100644
--- a/pyspades/vxl.pxd
+++ b/pyspades/vxl.pxd
@@ -26,6 +26,7 @@ cdef extern from "vxl_c.cpp":
int get_random_point(int x1, int y1, int x2, int y2, MapData * map,
float random_1, float random_2, int * x, int * y)
bint is_valid_position(int x, int y, int z)
+ void update_shadows(MapData * map)
cdef class VXLData:
cdef MapData * map
@@ -38,7 +39,8 @@ cdef class VXLData:
cpdef bint has_neighbors(self, int x, int y, int z)
cpdef bint is_surface(self, int x, int y, int z)
cpdef list get_neighbors(self, int x, int y, int z)
- cpdef bint check_node(self, int x, int y, int z, bint destroy = ?)
+ cpdef int check_node(self, int x, int y, int z, bint destroy = ?)
cpdef bint build_point(self, int x, int y, int z, tuple color)
cpdef bint set_column_fast(self, int x, int y, int start_z,
- int end_z, int end_color_z, int color)
\ No newline at end of file
+ int end_z, int end_color_z, int color)
+ cpdef update_shadows(self)
diff --git a/pyspades/vxl.pyx b/pyspades/vxl.pyx
index 21103d03..8a7d4967 100644
--- a/pyspades/vxl.pyx
+++ b/pyspades/vxl.pyx
@@ -118,16 +118,17 @@ cdef class VXLData:
def destroy_point(self, int x, int y, int z):
if not self.get_solid(x, y, z) or z >= 62:
- return False
+ return 0
set_point(x, y, z, self.map, 0, 0)
+ count = 1
start = time.time()
for node_x, node_y, node_z in self.get_neighbors(x, y, z):
if node_z < 62:
- self.check_node(node_x, node_y, node_z, True)
+ count += self.check_node(node_x, node_y, node_z, True)
taken = time.time() - start
if taken > 0.1:
print 'destroying block at', x, y, z, 'took:', taken
- return True
+ return count
def remove_point(self, int x, int y, int z):
if is_valid_position(x, y, z):
@@ -166,7 +167,7 @@ cdef class VXLData:
neighbors.append((node_x, node_y, node_z))
return neighbors
- cpdef bint check_node(self, int x, int y, int z, bint destroy = False):
+ cpdef int check_node(self, int x, int y, int z, bint destroy = False):
return check_node(x, y, z, self.map, destroy)
cpdef bint build_point(self, int x, int y, int z, tuple color):
@@ -193,6 +194,9 @@ cdef class VXLData:
set_column_color(x, y, z_start, z_color_end, self.map, color)
return True
+ cpdef update_shadows(self):
+ update_shadows(self.map)
+
def get_overview(self, int z = -1, bint rgba = False):
cdef unsigned int * data
cdef unsigned int i, r, g, b, a, color
diff --git a/pyspades/vxl_c.cpp b/pyspades/vxl_c.cpp
index 0fbb8c74..08c58413 100644
--- a/pyspades/vxl_c.cpp
+++ b/pyspades/vxl_c.cpp
@@ -149,13 +149,13 @@ int check_node(int x, int y, int z, MapData * map, int destroy)
z = current_node->z;
if (z >= 62) {
marked.clear();
- return 1;
+ return 0;
}
x = current_node->x;
y = current_node->y;
int i = get_pos(x, y, z);
-
+
// already visited?
pair::iterator, bool> ret;
ret = marked.insert(i);
@@ -180,8 +180,9 @@ int check_node(int x, int y, int z, MapData * map, int destroy)
}
}
+ int ret = (int)marked.size();
marked.clear();
- return 0;
+ return ret;
}
// write_map/save_vxl function from stb/nothings - thanks a lot for the
@@ -189,8 +190,8 @@ int check_node(int x, int y, int z, MapData * map, int destroy)
inline int is_surface(MapData * map, int x, int y, int z)
{
- if (z == 0) return 1;
if (map->geometry[get_pos(x, y, z)]==0) return 0;
+ if (z == 0) return 1;
if (x > 0 && map->geometry[get_pos(x-1, y, z)]==0) return 1;
if (x+1 < 512 && map->geometry[get_pos(x+1, y, z)]==0) return 1;
if (y > 0 && map->geometry[get_pos(x, y-1, z)]==0) return 1;
@@ -373,6 +374,37 @@ inline void get_random_point(int x1, int y1, int x2, int y2, MapData * map,
}
}
+#define SHADOW_DISTANCE 18
+#define SHADOW_STEP 2
+
+int sunblock(MapData * map, int x, int y, int z)
+{
+ int dec = SHADOW_DISTANCE;
+ int i = 127;
+
+ while (dec && z)
+ {
+ if (get_solid_wrap(x, --y, --z, map))
+ i -= dec;
+ dec -= SHADOW_STEP;
+ }
+ return i;
+}
+
+void update_shadows(MapData * map)
+{
+ int x, y, z;
+ int a;
+ unsigned int color;
+ for (map_type::iterator iter = map->colors.begin();
+ iter != map->colors.end(); ++iter) {
+ get_xyz(iter->first, &x, &y, &z);
+ color = iter->second;
+ a = sunblock(map, x, y, z);
+ iter->second = (color & 0x00FFFFFF) | (a << 24);
+ }
+}
+
struct MapGenerator
{
MapData * map;
diff --git a/pyspades/vxl_c.h b/pyspades/vxl_c.h
index 97a46717..d4271ea9 100644
--- a/pyspades/vxl_c.h
+++ b/pyspades/vxl_c.h
@@ -11,7 +11,7 @@
#define MAP_X 512
#define MAP_Y 512
#define MAP_Z 64
-#define get_pos(x, y, z) (x + (y) * MAP_Y + (z) * MAP_X * MAP_Y)
+#define get_pos(x, y, z) ((x) + (y) * MAP_Y + (z) * MAP_X * MAP_Y)
#define DEFAULT_COLOR 0xFF674028
struct MapData
@@ -21,6 +21,13 @@ struct MapData
map_type colors;
};
+void inline get_xyz(int pos, int* x, int* y, int* z)
+{
+ *x = pos % MAP_Y;
+ *y = (pos / MAP_Y) % MAP_X;
+ *z = pos / (MAP_X * MAP_Y);
+}
+
int inline is_valid_position(int x, int y, int z)
{
return x >= 0 && x < 512 && y >= 0 && y < 512 && z >= 0 && z < 64;
@@ -33,6 +40,15 @@ int inline get_solid(int x, int y, int z, MapData * map)
return map->geometry[get_pos(x, y, z)];
}
+int inline get_solid_wrap(int x, int y, int z, MapData * map)
+{
+ if (z < 0)
+ return 0;
+ else if (z >= 64)
+ return 1;
+ return map->geometry[get_pos(x & 511, y & 511, z)];
+}
+
int inline get_color(int x, int y, int z, MapData * map)
{
map_type::const_iterator iter = map->colors.find(
diff --git a/pyspades/world_c.cpp b/pyspades/world_c.cpp
index 6270c4ba..4b228791 100644
--- a/pyspades/world_c.cpp
+++ b/pyspades/world_c.cpp
@@ -125,7 +125,7 @@ int validate_hit(float shooter_x, float shooter_y, float shooter_z,
// silly VOXLAP function
inline void ftol(float f, long *a)
{
- *a = (long)f;
+ *a = (long)floor(f+0.5f);
}
//same as isvoxelsolid but water is empty && out of bounds returns true
diff --git a/tools/editor/run.py b/tools/editor/run.py
index 79d78ac0..8e474f99 100644
--- a/tools/editor/run.py
+++ b/tools/editor/run.py
@@ -24,8 +24,10 @@
import os
sys.path.append('../../')
+import re
import math
import subprocess
+from collections import OrderedDict
from PySide import QtGui, QtCore
from PySide.QtCore import Qt
from PySide.QtGui import QPainter
@@ -352,10 +354,17 @@ def draw(self, painter, old_x, old_y, x, y):
y - self.image.height() / 2.0,
self.image)
-TOOLS = {
- 'Brush' : Brush,
- 'Texture' : Texture
-}
+class Replace(Tool):
+ def draw(self, painter, old_x, old_y, x, y):
+ image = self.editor.image
+ color = struct.pack('I', self.editor.current_color.rgba())
+ target_color = struct.pack('I', image.pixel(x, y))
+ translate_color(image, color, target_color)
+
+TOOLS = OrderedDict()
+TOOLS['Brush'] = Brush
+TOOLS['Texture'] = Texture
+TOOLS['Replace color'] = Replace
class Settings(QtGui.QWidget):
def __init__(self, parent, *arg, **kw):
@@ -406,10 +415,8 @@ def freeze_image(self):
self.editor.toggle_freeze()
def set_color(self):
- color = QtGui.QColorDialog.getColor(
- Qt.white, self, 'Select a color',
- QtGui.QColorDialog.ShowAlphaChannel
- )
+ color = QtGui.QColorDialog.getColor(self.editor.color, self,
+ 'Select a color', QtGui.QColorDialog.ShowAlphaChannel)
self.editor.set_color(color)
def update_values(self):
@@ -501,8 +508,15 @@ def __init__(self, app, *arg, **kw):
self.file.addSeparator()
- self.voxed_action = QtGui.QAction('Open in &VOXED',
- self, shortcut = QtGui.QKeySequence('F5'), triggered = self.open_voxed)
+ self.bake_voxed_action = QtGui.QAction(
+ '&Calculate shadows and open in VOXED', self,
+ shortcut = QtGui.QKeySequence('F5'),
+ triggered = self.bake_shadows_open_voxed)
+ self.file.addAction(self.bake_voxed_action)
+
+ self.voxed_action = QtGui.QAction('Open in &VOXED', self,
+ shortcut = QtGui.QKeySequence('Shift+F5'),
+ triggered = self.open_voxed)
self.file.addAction(self.voxed_action)
self.file.addSeparator()
@@ -674,12 +688,14 @@ def save_as_selected(self):
self.save(name)
return name
- def save(self, filename):
- for z in xrange(0, 64):
- layer = self.layers[z]
+ def update_map(self):
+ for z, layer in enumerate(self.layers):
if layer.dirty:
self.map.set_overview(layer.data, z)
layer.dirty = False
+
+ def save(self, filename):
+ self.update_map()
open(filename, 'wb').write(self.map.generate())
self.set_dirty(False)
@@ -699,9 +715,15 @@ def open_voxed(self):
self.voxed_filename = exename
subprocess.call([self.voxed_filename, name])
+ def bake_shadows_open_voxed(self):
+ self.update_map()
+ self.map.update_shadows()
+ self.open_voxed()
+
def import_image_sequence(self):
name = QtGui.QFileDialog.getOpenFileName(self,
- 'Import image sequence (select any file from the sequence)', filter = IMAGE_OPEN_FILTER)[0]
+ 'Import image sequence (select any file from the sequence)',
+ filter = IMAGE_OPEN_FILTER)[0]
if not name:
return
root, ext = os.path.splitext(name)
@@ -709,7 +731,7 @@ def import_image_sequence(self):
path = os.path.join(root, head, tail[:-2])
old_z = self.edit_widget.z
progress = progress_dialog(self.edit_widget, 0, 63, 'Importing images...')
- for z in xrange(0, 64):
+ for z, layer in enumerate(reversed(self.layers)):
if progress.wasCanceled():
break
progress.setValue(z)
@@ -717,7 +739,7 @@ def import_image_sequence(self):
if not image:
continue
interpret_colorkey(image)
- self.layers[63 - z].set_image(image)
+ layer.set_image(image)
self.edit_widget.repaint()
self.set_dirty()
@@ -735,11 +757,10 @@ def export_color_map(self):
for x in xrange(0, 512):
height_found[y].append(False)
progress = progress_dialog(self.edit_widget, 0, 63, 'Exporting Colormap...')
- for z in xrange(0, 64):
+ for z, image in enumerate(self.layers):
if progress.wasCanceled():
break
progress.setValue(z)
- image = self.layers[z]
for y in xrange(0, 512):
image_line = image.scanLine(y)
color_line = color_lines[y]
@@ -760,7 +781,8 @@ def export_height_map(self):
height_packed = []
for z in xrange(0, 64):
height = (63 - z) * 4
- height_packed.append(struct.pack('I', QtGui.qRgba(height, height, height, 255)))
+ height_packed.append(struct.pack('I',
+ QtGui.qRgba(height, height, height, 255)))
height_image = QImage(512, 512, QImage.Format_ARGB32)
height_lines = []
height_found = []
@@ -770,12 +792,11 @@ def export_height_map(self):
for x in xrange(0, 512):
height_found[y].append(False)
progress = progress_dialog(self.edit_widget, 0, 63, 'Exporting Heightmap...')
- for z in xrange(0, 64):
+ for z, image in enumerate(self.layers):
if progress.wasCanceled():
break
progress.setValue(z)
packed_value = height_packed[z]
- image = self.layers[z]
for y in xrange(0, 512):
image_line = image.scanLine(y)
height_line = height_lines[y]
@@ -789,18 +810,20 @@ def export_height_map(self):
def export_image_sequence(self):
name = QtGui.QFileDialog.getSaveFileName(self,
- 'Export image sequence (select base filename)', filter = IMAGE_SAVE_FILTER)[0]
+ 'Export image sequence (select base filename)',
+ filter = IMAGE_SAVE_FILTER)[0]
if not name:
return
root, ext = os.path.splitext(name)
+ head, tail = os.path.split(root)
+ path = os.path.join(root, head, re.sub('\d{2}$', '', tail))
progress = progress_dialog(self.edit_widget, 0, 63, 'Exporting images...')
- for z in xrange(0, 64):
+ for z, image in enumerate(reversed(self.layers)):
if progress.wasCanceled():
break
progress.setValue(z)
- image = self.layers[63 - z]
new_image = make_colorkey(image)
- new_image.save(root + format(z, '02d') + ext)
+ new_image.save(path + format(z, '02d') + ext)
def copy_selected(self):
self.clipboard.setImage(self.edit_widget.image)
@@ -828,11 +851,10 @@ def paste_external_selected(self):
def mirror(self, hor, ver):
progress = progress_dialog(self.edit_widget, 0, 63, 'Mirroring...')
- for z in xrange(0, 64):
+ for z, layer in enumerate(self.layers):
if progress.wasCanceled():
break
progress.setValue(z)
- layer = self.layers[z]
layer.set_image(layer.mirrored(hor, ver))
self.edit_widget.repaint()
self.set_dirty()
@@ -857,11 +879,10 @@ def rotate(self, angle):
progress = progress_dialog(self.edit_widget, 0, 63, 'Rotating...')
transform = QtGui.QTransform()
transform.rotate(angle)
- for z in xrange(0, 64):
+ for z, layer in enumerate(self.layers):
if progress.wasCanceled():
break
progress.setValue(z)
- layer = self.layers[z]
layer.set_image(layer.transformed(transform))
self.edit_widget.repaint()
self.set_dirty()
@@ -893,8 +914,8 @@ def clear_selected(self):
self.set_dirty()
def get_height(self, color):
- return int(math.floor((float(QtGui.qRed(color)) + float(QtGui.qGreen(color)) +
- float(QtGui.qBlue(color)))/12.0))
+ return int(math.floor((float(QtGui.qRed(color)) +
+ float(QtGui.qGreen(color)) + float(QtGui.qBlue(color))) / 12.0))
def generate_heightmap(self, delete = False):
h_name = QtGui.QFileDialog.getOpenFileName(self,
@@ -916,15 +937,16 @@ def generate_heightmap(self, delete = False):
if not delete:
color_lines.append(c_image.scanLine(y))
for x in xrange(0,512):
- height_values[y].append(self.get_height(struct.unpack('I', height_line[x * 4:x * 4 + 4])[0]))
- progress = progress_dialog(self.edit_widget, 0, 63, 'Generating from heightmap...')
- for z in xrange(0, 64):
+ height_values[y].append(self.get_height(
+ struct.unpack('I', height_line[x * 4:x * 4 + 4])[0]))
+ progress = progress_dialog(self.edit_widget, 0, 63,
+ 'Generating from heightmap...')
+ for z, image in enumerate(reversed(self.layers)):
if progress.wasCanceled():
break
progress.setValue(z)
if z == 0 and delete:
continue
- image = self.layers[63 - z]
for y in xrange(0, 512):
image_line = image.scanLine(y)
if not delete:
@@ -949,10 +971,9 @@ def quit(self):
reply = QMessageBox.warning(self, self.app.applicationName(), text,
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,
QMessageBox.Yes)
- if reply == QMessageBox.Yes:
- if not self.save_selected():
- return
- elif reply == QMessageBox.Cancel:
+ if reply == QMessageBox.Cancel:
+ return
+ elif reply == QMessageBox.Yes and not self.save_selected():
return
self.app.exit()