From 71690535ac055dca651dfabdd3895f54e551c34e Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 31 May 2015 17:21:31 +1000 Subject: [PATCH 01/13] Add meta-conditions, change how condition types are defined - Meta-conditions allow other conditions to be defined before/after things like voice line additions - Condition flags/results added using decorators - Simplified the amount of variables passed around in main(), since they're globals anyway --- src/conditions.py | 260 ++++++++++++++++++++++++++++------------------ src/vbsp.py | 67 ++++-------- 2 files changed, 176 insertions(+), 151 deletions(-) diff --git a/src/conditions.py b/src/conditions.py index b7f1464e1..d5fffa432 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -1,33 +1,40 @@ +# coding: utf-8 import random from utils import Vec -import vbsp +from property_parser import Property +from instanceLocs import resolve as resolve_inst import vmfLib as VLib import utils -from instanceLocs import resolve as resolve_inst +# Stuff we get from VBSP in init() GLOBAL_INSTANCES = [] +OPTIONS = {} ALL_INST = set() -conditions = [] -STYLE_VARS = vbsp.settings['style_vars'] -VOICE_ATTR = vbsp.settings['has_attr'] + +conditions = [] +FLAG_LOOKUP = {} +RESULT_LOOKUP = {} -class SkipCondition(Exception): +class NextInstance(Exception): """Raised to skip to the next instance, from the SkipInstance result.""" pass +class EndCondition(Exception): + """Raised to skip the condition entirely, from the EndCond result.""" + pass + class Condition: - __slots__ = ['flags', 'results', 'else_results', 'priority', 'valid'] + __slots__ = ['flags', 'results', 'else_results', 'priority'] def __init__(self, flags=None, results=None, else_results=None, priority=0): self.flags = flags or [] self.results = results or [] self.else_results = else_results or [] self.priority = priority - self.valid = len(self.results) > 0 # is it valid? self.setup() def __repr__(self): @@ -96,21 +103,19 @@ def setup(self): def test(self, inst, remove_vmf=True): """Try to satisfy this condition on the given instance.""" - if not self.valid: - return success = True for flag in self.flags: - # utils.con_log('Flag: ' + repr(flag)) + utils.con_log('Flag: ' + repr(flag)) if not check_flag(flag, inst): success = False break - # utils.con_log(success) + utils.con_log(success) if remove_vmf: # our suffixes won't touch the .vmf extension inst['file'] = inst['file', ''][:-4] results = self.results if success else self.else_results - for res in results: + for res in results[:]: try: func = RESULT_LOOKUP[res.name] except KeyError: @@ -120,7 +125,9 @@ def test(self, inst, remove_vmf=True): ) ) else: - func(inst, res) + should_del = func(inst, res) + if should_del is True: + results.remove(res) if remove_vmf and not inst['file'].endswith('vmf'): inst['file'] += '.vmf' @@ -149,25 +156,75 @@ def __ge__(self, other): return self.priority >= other.priority return NotImplemented +def add_meta(func, priority, only_once=True): + """Add a metacondtion, which executes a function at a priority level. + + Used to allow users to allow adding conditions before or after a + transformation like the adding of quotes. + """ + # This adds a condition result like "func" (with quotes), which cannot + # be entered into property files. + # The qualname will be unique across modules. + name = '"' + func.__qualname__ + '"' + print("Adding metacondition (" + name + ")!") + + # Don't pass the prop_block onto the function, + # it doesn't contain any useful data. + RESULT_LOOKUP[name] = lambda inst, val: func(inst) + + cond = Condition( + results=[Property(name, '')], + priority=priority, + ) + + if only_once: + cond.results.append( + Property('endCondition', '') + ) + conditions.append(cond) + + +def meta_cond(priority=0, only_once=True): + """Decorator version of add_meta.""" + def x(func): + add_meta(func, priority, only_once) + return func + return x + +def make_flag(name): + """Decorator to add flags to the lookup.""" + def x(func): + FLAG_LOOKUP[name.casefold()] = func + return func + return x + +def make_result(name): + """Decorator to add results to the lookup.""" + def x(func): + RESULT_LOOKUP[name.casefold()] = func + return func + return x def add(prop_block): - """Add a condition to the list.""" + """Parse and add a condition to the list.""" con = Condition.parse(prop_block) - if con.valid: + if con.results or con.else_results: conditions.append(con) -def init(seed, inst_list, vmf_file, is_pre, game_mode): +def init(seed, inst_list, vmf_file): # Get a bunch of values from VBSP - global MAP_RAND_SEED, ALL_INST, VMF, IS_PREVIEW, GAME_MODE - IS_PREVIEW = is_pre - GAME_MODE = game_mode + import vbsp + global MAP_RAND_SEED, ALL_INST, VMF, STYLE_VARS, VOICE_ATTR, OPTIONS VMF = vmf_file MAP_RAND_SEED = seed ALL_INST = set(inst_list) + OPTIONS = vbsp.settings + STYLE_VARS = vbsp.settings['style_vars'] + VOICE_ATTR = vbsp.settings['has_attr'] - # Sort by priority, where higher = done earlier - conditions.sort(reverse=True) + # Sort by priority, where higher = done later + conditions.sort() def check_all(): @@ -178,14 +235,16 @@ def check_all(): for inst in VMF.by_class['func_instance']: try: condition.test(inst) - except SkipCondition: + except NextInstance: # This is raised to immediately stop running # this condition, and skip to the next instance. pass - if len(condition.results) == 0: + except EndCondition: + # This is raised to immediately stop running + # this condition, and skip to the next condtion. break - - remove_blank_inst() + if not condition.results and not condition.else_results: + break # Condition has run out of results, quit early utils.con_log('Map has attributes: ', [ key @@ -201,27 +260,15 @@ def check_inst(inst): """Run all conditions on a given instance.""" for condition in conditions: condition.test(inst) - remove_blank_inst() - - -def remove_blank_inst(): - """Remove instances with blank file attr. - - This allows conditions to strip the instances when requested. - """ - for inst in VMF.by_class['func_instance']: - # If set to "" in editoritems, we get ".vmf" instead - if inst['file', ''] in ('', '.vmf'): - VMF.remove_ent(inst) def check_flag(flag, inst): - # print('Checking {type} ({val!s} on {inst}'.format( - # type=flag.real_name, - # val=flag.value, - # inst=inst['file'], - # )) + print('Checking {type} ({val!s} on {inst}'.format( + type=flag.real_name, + val=flag.value, + inst=inst['file'], + )) try: func = FLAG_LOOKUP[flag.name] except KeyError: @@ -281,7 +328,7 @@ def add_output(inst, prop, target): # FLAGS # ######### - +@make_flag('and') def flag_and(inst, flag): for sub_flag in flag: if not check_flag(sub_flag, inst): @@ -290,35 +337,41 @@ def flag_and(inst, flag): return len(sub_flag.value) == 0 +@make_flag('or') def flag_or(inst, flag): for sub_flag in flag: if check_flag(sub_flag, inst): return True return False - +@make_flag('not') def flag_not(inst, flag): if len(flag.value) == 1: return not check_flag(flag[0], inst) return False +@make_flag('nor') def flag_nor(inst, flag): return not flag_or(inst, flag) +@make_flag('nand') def flag_nand(inst, flag): return not flag_and(inst, flag) +@make_flag('instance') def flag_file_equal(inst, flag): return inst['file'].casefold() in resolve_inst(flag.value) +@make_flag('InstFlag') def flag_file_cont(inst, flag): return flag.value in inst['file'].casefold() +@make_flag('hasInst') def flag_has_inst(_, flag): """Return true if the filename is present anywhere in the map.""" flags = resolve_inst(flag.value) @@ -329,37 +382,46 @@ def flag_has_inst(_, flag): ) +@make_flag('instVar') def flag_instvar(inst, flag): bits = flag.value.split(' ') return inst.fixup[bits[0]] == bits[1] +@make_flag('styleVar') def flag_stylevar(_, flag): return bool(STYLE_VARS[flag.value.casefold()]) +@make_flag('has') def flag_voice_has(_, flag): return bool(VOICE_ATTR[flag.value.casefold()]) +@make_flag('has_music') def flag_music(_, flag): - return vbsp.settings['options']['music_id'] == flag.value + return OPTIONS['music_id'] == flag.value +@make_flag('ifOption') def flag_option(_, flag): bits = flag.value.split(' ') key = bits[0].casefold() - if key in vbsp.settings['options']: - return vbsp.settings['options'][key] == bits[1] + if key in OPTIONS: + return OPTIONS[key] == bits[1] else: return False +@make_flag('ifMode') def flag_game_mode(_, flag): + from vbsp import GAME_MODE return GAME_MODE.casefold() == flag.value.casefold() +@make_flag('ifPreview') def flag_is_preview(_, flag): + from vbsp import IS_PREVIEW return IS_PREVIEW == utils.conv_bool(flag, False) ########### @@ -367,37 +429,47 @@ def flag_is_preview(_, flag): ########### +@make_result('rename') +@make_result('changeInstance') def res_change_instance(inst, res): """Set the file to a value.""" inst['file'] = resolve_inst(res.value)[0] +@make_result('suffix') def res_add_suffix(inst, res): """Add the specified suffix to the filename.""" inst['file'] += '_' + res.value +@make_result('styleVar') def res_set_style_var(_, res): for opt in res.value: if opt.name == 'settrue': STYLE_VARS[opt.value.casefold()] = True elif opt.name == 'setfalse': STYLE_VARS[opt.value.casefold()] = False + return True # Remove this result - +@make_result('has') def res_set_voice_attr(_, res): for opt in res.value: val = utils.conv_bool(opt.value, default=None) if val is not None: VOICE_ATTR[opt.name] = val + return True # Remove this result +@make_result('setOption') def res_set_option(_, res): for opt in res.value: - if opt.name in vbsp.settings['options']: - vbsp.settings['options'][opt.name] = opt.value + if opt.name in OPTIONS: + OPTIONS[opt.name] = opt.value + return True # Remove this result +@make_result('instVar') +@make_result('instVarSuffix') def res_add_inst_var(inst, res): """Append the value of an instance variable to the filename. @@ -416,12 +488,14 @@ def res_add_inst_var(inst, res): inst['file'] += '_' + inst.fixup[res.value, ''] +@make_result('setInstVar') def res_set_inst_var(inst, res): """Set an instance variable to the given value.""" var_name, val = res.value.split(' ') inst.fixup[var_name] = val +@make_result('variant') def res_add_variant(inst, res): """This allows using a random instance from a weighted group. @@ -436,6 +510,7 @@ def res_add_variant(inst, res): inst['file'] += "_var" + random.choice(res.value) +@make_result('addGlobal') def res_add_global_inst(_, res): """Add one instance in a location. @@ -461,9 +536,10 @@ def res_add_global_inst(_, res): new_inst['targetname'] = "inst_" new_inst.make_unique() VMF.add_ent(new_inst) - res.value = None # Disable this + return True # Remove this result +@make_result('addOverlay') def res_add_overlay_inst(inst, res): """Add another instance on top of this one.""" print('adding overlay', res['file']) @@ -477,6 +553,7 @@ def res_add_overlay_inst(inst, res): ) +@make_result('custOutput') def res_cust_output(inst, res): """Add an additional output to the instance with any values. @@ -524,11 +601,14 @@ def res_cust_output(inst, res): add_output(inst, out, targ) +@make_result('custAntline') def res_cust_antline(inst, res): """Customise the output antline texture, toggle instances. This allows adding extra outputs between the instance and the toggle. """ + import vbsp + over_name = '@' + inst['targetname'] + '_indicator' for over in ( VMF.by_class['info_overlay'] & @@ -561,6 +641,7 @@ def res_cust_antline(inst, res): break # Stop looking! +@make_result('faithMods') def res_faith_mods(inst, res): """Modify the trigger_catrapult that is created for ItemFaithPlate items. @@ -589,8 +670,10 @@ def res_faith_mods(inst, res): break # If we got here, we've found the output - stop scanning +@make_result('custFizzler') def res_cust_fizzler(base_inst, res): """Modify a fizzler item to allow for custom brush ents.""" + from vbsp import TEX_FIZZLER model_name = res['modelname', None] make_unique = utils.conv_bool(res['UniqueModel', '0']) fizz_name = base_inst['targetname', ''] @@ -685,7 +768,7 @@ def res_cust_fizzler(base_inst, res): for side in new_brush.sides(): try: side.mat = config[ - vbsp.TEX_FIZZLER[side.mat.casefold()] + TEX_FIZZLER[side.mat.casefold()] ] except (KeyError, IndexError): # If we fail, just use the original textures @@ -762,16 +845,25 @@ def convert_to_laserfield( side.vaxis = (" ".join(vaxis[:3]) + " 256] 0.25") +@make_result('condition') def res_sub_condition(base_inst, res): """Check a different condition if the outer block is true.""" res.value.test(base_inst, remove_vmf=False) +@make_result('nextInstance') def res_break(base_inst, res): """Skip to the next instance. """ - raise SkipCondition + raise NextInstance + +@make_result('endCondition') +def res_end_condition(base_inst, res): + """Skip to the next condition + + """ + raise EndCondition # For each direction, the two perpendicular axes and the axis it is pointing in. PAIR_AXES = { @@ -783,7 +875,7 @@ def res_break(base_inst, res): (0, 0, -1): 'xy' 'z', } - +@make_result('fizzlerModelPair') def res_fizzler_pair(begin_inst, res): """Modify the instance of a fizzler to link with its pair.""" orig_target = begin_inst['targetname'] @@ -845,55 +937,19 @@ def res_fizzler_pair(begin_inst, res): ) +@make_result('clearOutputs') +@make_result('clearOutput') def res_clear_outputs(inst, res): """Remove the outputs from an instance.""" inst.outputs.clear() +@meta_cond(priority=1000, only_once=False) +def remove_blank_inst(inst): + """Remove instances with blank file attr. -FLAG_LOOKUP = { - 'and': flag_and, - 'or': flag_or, - 'not': flag_not, - 'nor': flag_nor, - 'nand': flag_nand, - - 'instance': flag_file_equal, - 'instpart': flag_file_cont, - 'instvar': flag_instvar, - 'hasinst': flag_has_inst, - - 'stylevar': flag_stylevar, - - 'has': flag_voice_has, - 'hasmusic': flag_music, - - 'ifmode': flag_game_mode, - 'ifpreview': flag_is_preview, - 'ifoption': flag_option, - } - -RESULT_LOOKUP = { - "nextinstance": res_break, - "condition": res_sub_condition, - - "setoption": res_set_option, - "has": res_set_voice_attr, - "stylevar": res_set_style_var, - "clearoutputs": res_clear_outputs, - - "changeinstance": res_change_instance, - "addglobal": res_add_global_inst, - "addoverlay": res_add_overlay_inst, - - "suffix": res_add_suffix, - "variant": res_add_variant, - "instvar": res_add_inst_var, - "setinstvar": res_set_inst_var, - - "custoutput": res_cust_output, - "custantline": res_cust_antline, - "faithmods": res_faith_mods, - - "fizzlermodelpair": res_fizzler_pair, - "custfizzler": res_cust_fizzler, - } \ No newline at end of file + This allows conditions to strip the instances when requested. + """ + # If editoritems instances are set to "", PeTI will autocorrect it to + # ".vmf" - we need to handle that too. + if inst['file', ''] in ('', '.vmf'): + VMF.remove_ent(inst) \ No newline at end of file diff --git a/src/vbsp.py b/src/vbsp.py index deb60e1af..d6545194f 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -15,6 +15,7 @@ import utils import voiceLine import instanceLocs +import conditions # Configuration data extracted from VBSP_config settings = { @@ -31,7 +32,6 @@ "voice_data_coop": Property("Quotes_COOP", []), } -import conditions TEX_VALVE = { # all the non-wall textures produced by the Puzzlemaker, and their @@ -199,9 +199,8 @@ class ORIENT(Enum): BEE2_config = None -# A list of sucessful AddGlobal commands, so we can prevent adding the same -# instance twice. -global_instances = [] +GAME_MODE = 'ERR' +IS_PREVIEW = 'ERR' ################## # UTIL functions # @@ -364,8 +363,8 @@ def load_map(map_path): utils.con_log("Parsing complete!") +@conditions.meta_cond(priority=100) def add_voice(voice_config, mode): - print(mode) if mode == 'COOP': utils.con_log('Adding Coop voice lines!') data = settings['voice_data_coop'] @@ -392,11 +391,7 @@ def get_map_info(): - if in preview mode - timer values for entry/exit corridors """ - game_mode = 'ERR' - is_preview = 'ERR' - - # Timer_delay values for the entry/exit corridors, needed for quotes - voice_timer_pos = {} + global GAME_MODE, IS_PREVIEW inst_files = set() # Get a set of every instance in the map. file_coop_exit = instanceLocs.resolve('[coopExit]') @@ -405,58 +400,40 @@ def get_map_info(): file_coop_corr = instanceLocs.resolve('[coopCorr]') file_sp_entry_corr = instanceLocs.resolve('[spEntryCorr]') file_sp_exit_corr = instanceLocs.resolve('[spExitCorr]') - file_obs = instanceLocs.resolve('') - file_coop_entry = instanceLocs.resolve('[coopEntry]') for item in VMF.by_class['func_instance']: file = item['file'].casefold() if file in file_coop_exit: - game_mode = 'COOP' + GAME_MODE = 'COOP' elif file in file_sp_exit: - game_mode = 'SP' + GAME_MODE = 'SP' elif file in file_sp_entry: - is_preview = not utils.conv_bool(item.fixup['no_player_start']) - game_mode = 'SP' + IS_PREVIEW = not utils.conv_bool(item.fixup['no_player_start']) + GAME_MODE = 'SP' elif file in file_coop_corr: is_preview = not utils.conv_bool(item.fixup['no_player_start']) - voice_timer_pos['exit'] = ( - item.fixup['timer_delay', '0'] - ) - game_mode = 'COOP' + GAME_MODE = 'COOP' elif file in file_sp_entry_corr: - voice_timer_pos['entry'] = ( - item.fixup['timer_delay', '0'] - ) - is_preview = not utils.conv_bool(item.fixup['no_player_start']) + IS_PREVIEW = not utils.conv_bool(item.fixup['no_player_start']) elif file in file_sp_exit_corr: - voice_timer_pos['exit'] = ( - item.fixup['timer_delay', '0'] - ) - elif file in file_coop_entry: - voice_timer_pos['entry'] = ( - item.fixup['timer_delay', '0'] - ) - elif file in file_obs: - voice_timer_pos['obs'] = ( - item.fixup['timer_delay', '0'] - ) + IS_PREVIEW = not utils.conv_bool(item.fixup['no_player_start']) inst_files.add(item['file']) - utils.con_log("Game Mode: " + game_mode) - utils.con_log("Is Preview: " + str(is_preview)) + utils.con_log("Game Mode: " + GAME_MODE) + utils.con_log("Is Preview: " + str(IS_PREVIEW)) - if game_mode == 'ERR': + if GAME_MODE == 'ERR': raise Exception( 'Unknown game mode - Map missing exit room!' ) - if is_preview == 'ERR': + if IS_PREVIEW == 'ERR': raise Exception( "Can't determine if preview is enabled " '- Map likely missing entry room!' ) - return is_preview, game_mode, voice_timer_pos, inst_files + return inst_files def calc_rand_seed(): @@ -1430,19 +1407,12 @@ def main(): MAP_SEED = calc_rand_seed() - ( - IS_PREVIEW, - GAME_MODE, - voice_timer_pos, - all_inst, - ) = get_map_info() + all_inst = get_map_info() conditions.init( seed=MAP_SEED, inst_list=all_inst, vmf_file=VMF, - game_mode=GAME_MODE, - is_pre=IS_PREVIEW, ) fix_inst() @@ -1457,7 +1427,6 @@ def main(): change_func_brush() fix_worldspawn() - add_voice(voice_timer_pos, GAME_MODE) save(new_path) run_vbsp( From 248fc540af3fe659e26d8f6cc045d6372def87a6 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Mon, 1 Jun 2015 18:34:12 +1000 Subject: [PATCH 02/13] Remove timer option on voicelines, fix conditions - The condition.valid attribute no longer exists - Timer option will just be confusing for users --- src/conditions.py | 32 +++++++++++++------------------- src/vbsp.py | 9 ++++----- src/voiceLine.py | 17 +---------------- 3 files changed, 18 insertions(+), 40 deletions(-) diff --git a/src/conditions.py b/src/conditions.py index d5fffa432..6126a490c 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -105,7 +105,6 @@ def test(self, inst, remove_vmf=True): """Try to satisfy this condition on the given instance.""" success = True for flag in self.flags: - utils.con_log('Flag: ' + repr(flag)) if not check_flag(flag, inst): success = False break @@ -231,20 +230,19 @@ def check_all(): """Check all conditions.""" utils.con_log('Checking Conditions...') for condition in conditions: - if condition.valid: - for inst in VMF.by_class['func_instance']: - try: - condition.test(inst) - except NextInstance: - # This is raised to immediately stop running - # this condition, and skip to the next instance. - pass - except EndCondition: - # This is raised to immediately stop running - # this condition, and skip to the next condtion. - break - if not condition.results and not condition.else_results: - break # Condition has run out of results, quit early + for inst in VMF.by_class['func_instance']: + try: + condition.test(inst) + except NextInstance: + # This is raised to immediately stop running + # this condition, and skip to the next instance. + pass + except EndCondition: + # This is raised to immediately stop running + # this condition, and skip to the next condtion. + break + if not condition.results and not condition.else_results: + break # Condition has run out of results, quit early utils.con_log('Map has attributes: ', [ key @@ -665,10 +663,6 @@ def res_faith_mods(inst, res): if fixup_var: inst.fixup[fixup_var] = 'straight' break - else: - continue # Check the next trigger - break # If we got here, we've found the output - stop scanning - @make_result('custFizzler') def res_cust_fizzler(base_inst, res): diff --git a/src/vbsp.py b/src/vbsp.py index d6545194f..0477bd951 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -364,11 +364,11 @@ def load_map(map_path): @conditions.meta_cond(priority=100) -def add_voice(voice_config, mode): - if mode == 'COOP': +def add_voice(inst): + if GAME_MODE == 'COOP': utils.con_log('Adding Coop voice lines!') data = settings['voice_data_coop'] - elif mode == 'SP': + elif GAME_MODE == 'SP': utils.con_log('Adding Singleplayer voice lines!') data = settings['voice_data_sp'] else: @@ -379,8 +379,7 @@ def add_voice(voice_config, mode): has_items=settings['has_attr'], style_vars_=settings['style_vars'], vmf_file=VMF, - timer_config=voice_config, - mode=mode, + mode=GAME_MODE, ) diff --git a/src/voiceLine.py b/src/voiceLine.py index 23e889763..eaed2a066 100644 --- a/src/voiceLine.py +++ b/src/voiceLine.py @@ -105,7 +105,6 @@ def add_voice( has_items, style_vars_, vmf_file, - timer_config=utils.EmptyMapping, mode='SP', ): """Add a voice line to the map.""" @@ -162,24 +161,10 @@ def add_voice( ) if possible_quotes: - # If we have a timer value, we go that many back from the - # highest priority item. If it fails, default to the first. - timer_val = timer_config.get( - group['config', ''].casefold(), - '3') - try: - timer_val = int(timer_val) - 3 - except ValueError: - timer_val = 0 choreo_loc = group['choreo_loc', quote_loc] - utils.con_log('Timer value: ', timer_val) - - try: - chosen = possible_quotes[timer_val][1] - except IndexError: - chosen = possible_quotes[0][1] + chosen = possible_quotes[0][1] utils.con_log('Chosen: {!r}'.format(chosen)) From 6d8e456d8f87ca6f1656b8f6c16683d1b5307ba9 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Mon, 1 Jun 2015 18:35:04 +1000 Subject: [PATCH 03/13] Add ability to offset faith plate trigger Used for some faith plate instances which extend into the map slightly, so they can have correct collisions --- src/conditions.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/conditions.py b/src/conditions.py index 6126a490c..84390956a 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -646,8 +646,21 @@ def res_faith_mods(inst, res): """ # Get data about the trigger this instance uses for flinging fixup_var = res['instvar', ''] + offset = utils.conv_int(res['raise_trig', '0']) + if offset: + angle = Vec.from_str(inst['angles', '0 0 0']) + offset = round(Vec(0, 0, offset).rotate(angle.x, angle.y, angle.z)) + ':type offset Vec' for trig in VMF.by_class['trigger_catapult']: if inst['targetname'] in trig['targetname']: + if offset: # Edit both the normal and the helper trigger + trig['origin'] = ( + Vec.from_str(trig['origin']) + + offset + ).join(' ') + for solid in trig.solids: + solid.translate(offset) + for out in trig.outputs: if out.inst_in == 'animate_angled_relay': out.inst_in = res['angled_targ', 'animate_angled_relay'] From 73116e526f612da4a060aaed4cc702ca47b00ed5 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Wed, 3 Jun 2015 18:51:36 +1000 Subject: [PATCH 04/13] Use set for global_instances memory That'll make future `in` checks much faster --- src/conditions.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/conditions.py b/src/conditions.py index 84390956a..9940fe3cf 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -8,7 +8,7 @@ import utils # Stuff we get from VBSP in init() -GLOBAL_INSTANCES = [] +GLOBAL_INSTANCES = set() OPTIONS = {} ALL_INST = set() @@ -515,8 +515,9 @@ def res_add_global_inst(_, res): Once this is executed, it will be ignored thereafter. """ if res.value is not None: - if (res['file'] not in GLOBAL_INSTANCES or - utils.conv_bool(res['allow_multiple', '0'], True)): + if ( + utils.conv_bool(res['allow_multiple', '0']) or + res['file'] not in GLOBAL_INSTANCES): # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid @@ -529,7 +530,7 @@ def res_add_global_inst(_, res): "origin": res['position', '0 0 -10000'], "fixup_style": res['fixup_style', '0'], }) - GLOBAL_INSTANCES.append(res['file']) + GLOBAL_INSTANCES.add(res['file']) if new_inst['targetname'] == '': new_inst['targetname'] = "inst_" new_inst.make_unique() From a75bd2ecd00610255c457e97bdc23081fb06e027 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Wed, 3 Jun 2015 18:52:56 +1000 Subject: [PATCH 05/13] Only remove vmf suffix when needed This ensures we put it back later, and prevents double-removing it. --- src/conditions.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/conditions.py b/src/conditions.py index 9940fe3cf..7603e349b 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -101,7 +101,7 @@ def setup(self): elif res.name == 'condition': res.value = Condition.parse(res) - def test(self, inst, remove_vmf=True): + def test(self, inst): """Try to satisfy this condition on the given instance.""" success = True for flag in self.flags: @@ -109,10 +109,6 @@ def test(self, inst, remove_vmf=True): success = False break utils.con_log(success) - if remove_vmf: - # our suffixes won't touch the .vmf extension - inst['file'] = inst['file', ''][:-4] - results = self.results if success else self.else_results for res in results[:]: try: @@ -128,9 +124,6 @@ def test(self, inst, remove_vmf=True): if should_del is True: results.remove(res) - if remove_vmf and not inst['file'].endswith('vmf'): - inst['file'] += '.vmf' - def __lt__(self, other): """Condition items sort by priority.""" if hasattr(other, 'priority'): @@ -322,6 +315,15 @@ def add_output(inst, prop, target): inst_out=prop['targ_out', ''], )) + +def add_suffix(inst, suff): + """Append the given suffix to the instance. + """ + file = inst['file'] + utils.con_log(file) + old_name, dot, ext = file.partition('.') + inst['file'] = ''.join((old_name, suff, dot, ext)) + ######### # FLAGS # ######### @@ -437,7 +439,7 @@ def res_change_instance(inst, res): @make_result('suffix') def res_add_suffix(inst, res): """Add the specified suffix to the filename.""" - inst['file'] += '_' + res.value + add_suffix(inst, '_' + res.value) @make_result('styleVar') @@ -480,10 +482,10 @@ def res_add_inst_var(inst, res): if rep.name == 'variable': continue # this isn't a lookup command! if rep.name == val: - inst['file'] += '_' + rep.value + add_suffix(inst, '_' + rep.value) break else: # append the value - inst['file'] += '_' + inst.fixup[res.value, ''] + add_suffix(inst, '_' + inst.fixup[res.value, '']) @make_result('setInstVar') @@ -505,7 +507,7 @@ def res_add_variant(inst, res): random.seed(MAP_RAND_SEED + inst['origin'] + inst['angles']) else: random.seed(inst['targetname']) - inst['file'] += "_var" + random.choice(res.value) + add_suffix(inst, "_var" + random.choice(res.value)) @make_result('addGlobal') @@ -856,7 +858,7 @@ def convert_to_laserfield( @make_result('condition') def res_sub_condition(base_inst, res): """Check a different condition if the outer block is true.""" - res.value.test(base_inst, remove_vmf=False) + res.value.test(base_inst) @make_result('nextInstance') From a0e8ece9ecad0ccc5c1b82e982c3ff36f222c9ae Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Wed, 3 Jun 2015 18:53:50 +1000 Subject: [PATCH 06/13] Add defaults for add_overlay If the instance doesn't define a targetname or angles (elevator instances) they'll just get 'None', which doesn't make any sense. --- src/conditions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conditions.py b/src/conditions.py index 7603e349b..68cce6439 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -546,9 +546,9 @@ def res_add_overlay_inst(inst, res): print('adding overlay', res['file']) VMF.create_ent( classname='func_instance', - targetname=inst['targetname'], + targetname=inst['targetname', ''], file=resolve_inst(res['file', ''])[0], - angles=inst['angles'], + angles=inst['angles', '0 0 0'], origin=inst['origin'], fixup_style=res['fixup_style', '0'], ) From ae8c324f3900e78e49ecbdce828abf65b5cd34b3 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 4 Jun 2015 17:51:22 +1000 Subject: [PATCH 07/13] Properly load last selected palette item --- src/UI.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/UI.py b/src/UI.py index fa7ef64cc..8a88b69fb 100644 --- a/src/UI.py +++ b/src/UI.py @@ -1759,4 +1759,6 @@ def style_select_callback(style_id): style_win.callback = style_select_callback style_select_callback(style_win.chosen_id) + set_palette() + event_loop = TK_ROOT.mainloop \ No newline at end of file From 40f23f512ffa248a2640de7ae2a181cb49de7893 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 4 Jun 2015 17:53:40 +1000 Subject: [PATCH 08/13] Improve fixup logic - Correctly determine next ID to use on __setitem__ - Wrong __contains__ logic (Will never return True) - just use __iter__ fallback version - Add condition to remove fixup vars (useful if hitting fixup limit of 10) --- src/conditions.py | 15 ++++++++++----- src/vmfLib.py | 9 +++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/conditions.py b/src/conditions.py index 68cce6439..88b530e6c 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -255,11 +255,11 @@ def check_inst(inst): def check_flag(flag, inst): - print('Checking {type} ({val!s} on {inst}'.format( - type=flag.real_name, - val=flag.value, - inst=inst['file'], - )) + # print('Checking {type} ({val!s} on {inst}'.format( + # type=flag.real_name, + # val=flag.value, + # inst=inst['file'], + # )) try: func = FLAG_LOOKUP[flag.name] except KeyError: @@ -953,6 +953,11 @@ def res_clear_outputs(inst, res): """Remove the outputs from an instance.""" inst.outputs.clear() +@make_result('removeFixup') +def res_rem_fixup(inst, res): + """Remove a fixup from the instance.""" + del inst.fixup['res'] + @meta_cond(priority=1000, only_once=False) def remove_blank_inst(inst): """Remove instances with blank file attr. diff --git a/src/vmfLib.py b/src/vmfLib.py index 629e663b0..41842e9d8 100644 --- a/src/vmfLib.py +++ b/src/vmfLib.py @@ -1305,10 +1305,6 @@ def get(self, var, default: str=None): def copy_dict(self): return self._fixup.copy() - def __contains__(self, var: str): - """Determine if this instance has the named $replace variable.""" - return var.casefold() in self._fixup - def __getitem__(self, key): if isinstance(key, tuple): return self.get(key[0], default=key[1]) @@ -1322,11 +1318,12 @@ def __setitem__(self, var, val): if var[0] == '$': var = var[1:] folded_var = var.casefold() - if folded_var not in self._fixup: - max_id = 1 + if folded_var not in self: + max_id = 0 for fixup in self._fixup.values(): if int(fixup.id) > max_id: max_id = int(fixup.id) + max_id += 1 if max_id < 9: max_id = "0" + str(max_id) else: From 6c5565a4258f87764405fb4edf4f307ff4e2909c Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Fri, 5 Jun 2015 18:17:10 +1000 Subject: [PATCH 09/13] Use infinity symbol for timer=0 display - Tkinter does support some Unicode --- src/itemPropWin.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/itemPropWin.py b/src/itemPropWin.py index 229ed610e..a9cc6fc21 100644 --- a/src/itemPropWin.py +++ b/src/itemPropWin.py @@ -1,3 +1,4 @@ +#coding: utf-8 from tkinter import * # ui library from tk_root import TK_ROOT from tkinter import ttk # themed ui components that match the OS @@ -54,8 +55,8 @@ 'autorespawn', ] -widgets = {} # holds the checkbox or other item used to manipulate the box -labels = {} # holds the descriptive labels for each property +widgets = {} # holds the checkbox or other item used to manipulate the box +labels = {} # holds the descriptive labels for each property propList = [] @@ -153,8 +154,8 @@ def save_tim(key, val): enable_tim_callback = True labels[key]['text'] = ( - 'Timer Delay:\n (' + - ('Inf' if new_val == 0 else str(new_val)) + ')' + 'Timer Delay:\n ({})'.format( + '∞' if new_val == 0 else str(new_val)) ) if new_val > values[key]: From edc5b0ab9b2d643f76eae2590f032db064743eb6 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Fri, 5 Jun 2015 18:18:51 +1000 Subject: [PATCH 10/13] Add corridor list to the Compile Pane This lets users choose which corridor type is chosen, or leave as the default. Styles can add labels for each of the corridors to explain what they are. --- src/CompilerPane.py | 112 ++++++++++++++++++++++++++++++++++++++++--- src/UI.py | 4 ++ src/packageLoader.py | 14 ++++++ 3 files changed, 123 insertions(+), 7 deletions(-) diff --git a/src/CompilerPane.py b/src/CompilerPane.py index 97653675d..48c083690 100644 --- a/src/CompilerPane.py +++ b/src/CompilerPane.py @@ -3,7 +3,7 @@ from tkinter import ttk from tkinter import filedialog -import os +from functools import partial import img as png @@ -24,6 +24,11 @@ 'spawn_elev': 'True', 'player_model': 'PETI', }, + 'Corridor': { + 'sp_entry': '1', + 'sp_exit': '1', + 'coop': '1', + }, 'Counts': { 'brush': '0', 'ent': '0', @@ -62,6 +67,42 @@ count_ents = IntVar(value=0) count_overlay = IntVar(value=0) +CORRIDOR = {} + +def set_corr_values(group_name, props): + """Set the corrdors according to the passed prop_block.""" + count = 7 if group_name == 'sp_entry' else 4 + group = CORRIDOR[group_name] = ['Random'] + [ + str(i) + ': Corridor' + for i in + range(1, count+1) + ] + for prop in props[group_name]: + try: + ind = int(prop.name) + except ValueError: + continue + + if 0 < ind <= count: + group[ind] = '{!r}: {}'.format(ind, prop.value) + + +def make_corr_combo(frm, corr_name, width): + set_corr_values(corr_name, {corr_name: []}) + widget = ttk.Combobox( + frm, + values=CORRIDOR[corr_name], + width=width, + exportselection=0, + ) + widget['postcommand'] = partial(set_corr_dropdown, corr_name, widget) + widget.state(['readonly']) + widget.bind( + '<>', + partial(set_corr, corr_name) + ) + widget.current(COMPILE_CFG.get_int('Corridor', corr_name)) + return widget def refresh_counts(reload=True): @@ -123,6 +164,13 @@ def set_model(_=None): COMPILE_CFG['General']['player_model'] = PLAYER_MODELS_REV[text] COMPILE_CFG.save() +def set_corr(corr_name, e): + COMPILE_CFG['Corridor'][corr_name] = str(e.widget.current()) + COMPILE_CFG.save() + +def set_corr_dropdown(corr_name, widget): + """Set the values in the dropdown when it's opened.""" + widget['values'] = CORRIDOR[corr_name] def make_pane(tool_frame): """Create the compiler options pane. @@ -132,9 +180,9 @@ def make_pane(tool_frame): window = SubPane( TK_ROOT, options=GEN_OPTS, - title='Compile Options', + title='Compile Opt', name='compiler', - resize_x=False, + resize_x=True, resize_y=False, tool_frame=tool_frame, tool_img=png.png('icons/win_compiler'), @@ -207,6 +255,8 @@ def make_pane(tool_frame): ) elev_frame.grid(row=1, column=0, sticky=EW) + elev_frame.columnconfigure(0, weight=1) + elev_frame.columnconfigure(1, weight=1) UI['elev_preview'] = ttk.Radiobutton( elev_frame, @@ -227,23 +277,71 @@ def make_pane(tool_frame): UI['elev_preview'].grid(row=0, column=0, sticky=W) UI['elev_elevator'].grid(row=0, column=1, sticky=W) + corr_frame = ttk.LabelFrame( + window, + text='Corridor:', + labelanchor=N, + ) + corr_frame.grid(row=2, column=0, sticky=EW) + corr_frame.columnconfigure(0, weight=1) + corr_frame.columnconfigure(1, weight=1) + + UI['corr_sp_entry'] = make_corr_combo( + corr_frame, + 'sp_entry', + width=9, + ) + + UI['corr_sp_exit'] = make_corr_combo( + corr_frame, + 'sp_exit', + width=9, + ) + + UI['corr_coop'] = make_corr_combo( + corr_frame, + 'coop', + width=9, + ) + + UI['corr_sp_entry'].grid(row=1, column=0, sticky=EW) + UI['corr_sp_exit'].grid(row=1, column=1, sticky=EW) + UI['corr_coop'].grid(row=2, column=1, sticky=EW) + ttk.Label( + corr_frame, + text='SP Entry:', + anchor=CENTER, + ).grid(row=0, column=0, sticky=EW) + ttk.Label( + corr_frame, + text='SP Exit:', + anchor=CENTER, + ).grid(row=0, column=1, sticky=EW) + ttk.Label( + corr_frame, + text='Coop:', + anchor=CENTER, + ).grid(row=2, column=0, sticky=EW) + model_frame = ttk.LabelFrame( window, text='Player Model (SP):', labelanchor=N, ) - model_frame.grid(row=2, column=0, sticky=EW) + model_frame.grid(row=3, column=0, sticky=EW) UI['player_mdl'] = ttk.Combobox( model_frame, exportselection=0, textvariable=player_model_var, values=PLAYER_MODEL_ORDER, + width=20, ) # Users can only use the dropdown UI['player_mdl'].state(['readonly']) UI['player_mdl'].grid(row=0, column=0, sticky=EW) UI['player_mdl'].bind('<>', set_model) + model_frame.columnconfigure(0, weight=1) count_frame = ttk.LabelFrame( window, @@ -251,7 +349,9 @@ def make_pane(tool_frame): labelanchor=N, ) - count_frame.grid(row=3, column=0) + count_frame.grid(row=4, column=0, sticky=EW) + count_frame.columnconfigure(0, weight=1) + count_frame.columnconfigure(2, weight=1) ttk.Label( count_frame, @@ -273,7 +373,6 @@ def make_pane(tool_frame): padx=5, ) - ttk.Label( count_frame, text='Overlay', @@ -293,7 +392,6 @@ def make_pane(tool_frame): command=refresh_counts, ).grid(row=3, column=1) - ttk.Label( count_frame, text='Brush', diff --git a/src/UI.py b/src/UI.py index 8a88b69fb..29a13849d 100644 --- a/src/UI.py +++ b/src/UI.py @@ -1749,6 +1749,10 @@ def style_select_callback(style_id): # Disable this if the style doesn't have elevators elev_win.readonly = not style_obj.has_video + CompilerPane.set_corr_values('sp_entry', style_obj.corridor_names) + CompilerPane.set_corr_values('sp_exit', style_obj.corridor_names) + CompilerPane.set_corr_values('coop', style_obj.corridor_names) + sugg = style_obj.suggested win_types = (voice_win, music_win, skybox_win, elev_win) for win, sugg_val in zip(win_types, sugg): diff --git a/src/packageLoader.py b/src/packageLoader.py index 1ebd44687..867b15c22 100644 --- a/src/packageLoader.py +++ b/src/packageLoader.py @@ -400,6 +400,7 @@ def __init__( short_name=None, suggested=None, has_video=True, + corridor_names=utils.EmptyMapping, ): self.id = style_id self.auth = author @@ -412,6 +413,11 @@ def __init__( self.bases = [] # Set by setup_style_tree() self.suggested = suggested or {} self.has_video = has_video + self.corridor_names = { + 'sp_entry': corridor_names.get('sp_entry', Property('', [])), + 'sp_exit': corridor_names.get('sp_exit', Property('', [])), + 'coop': corridor_names.get('coop', Property('', [])), + } if config is None: self.config = Property(None, []) else: @@ -434,6 +440,13 @@ def parse(cls, data): sugg['elev', ''], ) + corridors = info.find_key('corridors', []) + corridors = { + 'sp_entry': corridors.find_key('sp_entry', []), + 'sp_exit': corridors.find_key('sp_exit', []), + 'coop': corridors.find_key('coop', []), + } + short_name = selitem_data.short_name or None if base == '': base = None @@ -459,6 +472,7 @@ def parse(cls, data): short_name=short_name, suggested=sugg, has_video=has_video, + corridor_names=corridors, ) def add_over(self, override): From 3d2b7cdffefdf1e36694de14ca280eba21072bb3 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sat, 6 Jun 2015 09:25:02 +1000 Subject: [PATCH 11/13] Return instanceLocs in correct order --- src/instanceLocs.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/instanceLocs.py b/src/instanceLocs.py index dab8bcfdc..47156c793 100644 --- a/src/instanceLocs.py +++ b/src/instanceLocs.py @@ -95,7 +95,6 @@ def load_conf(): for inst in prop ] - INST_SPECIAL = { key.casefold(): resolve(val_string) for key, val_string in @@ -136,7 +135,7 @@ def resolve(path) -> list: '"{}" not a valid item!'.format(item) ) return [] - out = set() + out = [] for val in subitem.split(','): ind = SUBITEMS.get( val.strip().casefold(), @@ -144,8 +143,8 @@ def resolve(path) -> list: ) # Only add if it's actually in range if 0 <= ind < len(item_values): - out.add(item_values[ind]) - return list(out) + out.append(item_values[ind]) + return out else: try: return INSTANCE_FILES[path] From 80f0f8dc70b84a2f83a8235b6f08aece03d0dc99 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sat, 6 Jun 2015 13:05:31 +1000 Subject: [PATCH 12/13] Send correct BEE2 location to VBSP - Our working directory is src/ or bin/, not the BEE2/ folder we want to save. --- src/gameMan.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gameMan.py b/src/gameMan.py index 447b7582b..4d128baa8 100644 --- a/src/gameMan.py +++ b/src/gameMan.py @@ -283,7 +283,9 @@ def export( vbsp_config.set_key(('Options', 'music_ID'), music.id) vbsp_config += music.config - vbsp_config.set_key(('Options', 'BEE2_loc'), os.getcwd()) + vbsp_config.set_key(('Options', 'BEE2_loc'), + os.path.dirname(os.getcwd()) # Go up one dir to our actual location + ) # If there are multiple of these blocks, merge them together vbsp_config.merge_children('Conditions', From abcff70c4ad0f146255649e23063ff06921d9c62 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sat, 6 Jun 2015 13:07:07 +1000 Subject: [PATCH 13/13] Set entry/exit hallways and preview mode options --- src/vbsp.py | 52 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/src/vbsp.py b/src/vbsp.py index 0477bd951..db7de0b08 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -386,9 +386,11 @@ def add_voice(inst): def get_map_info(): """Determine various attributes about the map. + This also set the 'preview in elevator' options and forces + a particular entry/exit hallway. + - SP/COOP status - if in preview mode - - timer values for entry/exit corridors """ global GAME_MODE, IS_PREVIEW @@ -400,22 +402,52 @@ def get_map_info(): file_sp_entry_corr = instanceLocs.resolve('[spEntryCorr]') file_sp_exit_corr = instanceLocs.resolve('[spExitCorr]') + # Should we force the player to spawn in the elevator? + elev_override = BEE2_config.get_bool('General', 'spawn_elev') + + if elev_override: + # Make conditions set appropriately + utils.con_log('Forcing elevator spawn!') + IS_PREVIEW = False + + no_player_start_inst = ( + # All the instances that have the no_player start value + file_sp_entry + + file_coop_corr + + file_sp_entry_corr + + file_sp_exit_corr + ) + override_sp_entry = BEE2_config.get_int('Corridor', 'sp_entry', 0) + override_sp_exit = BEE2_config.get_int('Corridor', 'sp_exit', 0) + override_coop_corr = BEE2_config.get_int('Corridor', 'coop', 0) + utils.con_log(override_sp_exit, override_sp_entry, override_coop_corr) for item in VMF.by_class['func_instance']: file = item['file'].casefold() - if file in file_coop_exit: + if file in no_player_start_inst: + if elev_override: + item.fixup['no_player_start'] = '1' + else: + IS_PREVIEW = not utils.conv_bool(item.fixup['no_player_start']) + + if file in file_sp_exit_corr: + if override_sp_exit != 0: + utils.con_log('Setting exit to ' + str(override_sp_exit)) + item['file'] = file_sp_exit_corr[override_sp_exit-1] + elif file in file_sp_entry_corr: + if override_sp_entry != 0: + utils.con_log('Setting entry to ' + str(override_sp_entry)) + item['file'] = file_sp_entry_corr[override_sp_entry-1] + elif file in file_coop_corr: + GAME_MODE = 'COOP' + if override_coop_corr != 0: + utils.con_log('Setting coop exit to ' + str(override_coop_corr)) + item['file'] = file_coop_corr[override_coop_corr-1] + elif file in file_coop_exit: GAME_MODE = 'COOP' elif file in file_sp_exit: GAME_MODE = 'SP' elif file in file_sp_entry: - IS_PREVIEW = not utils.conv_bool(item.fixup['no_player_start']) GAME_MODE = 'SP' - elif file in file_coop_corr: - is_preview = not utils.conv_bool(item.fixup['no_player_start']) - GAME_MODE = 'COOP' - elif file in file_sp_entry_corr: - IS_PREVIEW = not utils.conv_bool(item.fixup['no_player_start']) - elif file in file_sp_exit_corr: - IS_PREVIEW = not utils.conv_bool(item.fixup['no_player_start']) inst_files.add(item['file'])