From 810d2e480f2da23e9ec2332dc6c51c94d2a5993c Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Mon, 3 Aug 2015 16:43:22 +1000 Subject: [PATCH 01/30] Start adding posIsSolid flag --- src/conditions.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/conditions.py b/src/conditions.py index eef293c45..f09323428 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -700,6 +700,44 @@ def flag_angles(inst, flag): return inst_normal == normal or ( allow_inverse and -inst_normal == normal ) + +@make_flag('posIsSolid') +def flag_brush_at_loc(inst, flag): + """Checks to see if a wall is present at the given location. + + - Pos is the position of the brush, where `0 0 0` is the floor-position + of the brush, and '0 0 1' is the ceiling. + - Dir is the normal the face is pointing. (0 0 1) is 'up'. + - Type defines the type the brush must be: + - "Any" requires either a black or white brush. + - "None" means that no brush must be present. + - "White" requires a portalable surface. + - "Black" requires a non-portalable surface. + - SetVar defines an instvar which will be given a value of "black", + 'white" or "none" to allow the result to be reused. + - RemoveBrush: If set to 1, the brush will be removed if found. + """ + pos = Vec.from_str(inst['origin', '0 0 0']) + # Relative to the instance origin + pos += ( + Vec.from_str(flag['pos', '0 0 0']) + * 128 # 128 units between blocks + - (0, 0, 64) # Subtract so origin is the floor-position + ).rotate_by_str( + inst['angles', '0 0 0'] + ) + + norm = Vec.from_str(flag['dir', '0 0 1']).rotate_by_str( + inst['angles', '0 0 0'] + ) + + result_var = flag['setVar', ''] + should_remove = utils.conv_bool(flag['RemoveBrush']) + des_type = flag['type', 'any'].casefold() + + + + ########### # RESULTS # ########### From c844476ead416a0ada31a1076a0c2b18751c0d50 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Mon, 3 Aug 2015 16:50:27 +1000 Subject: [PATCH 02/30] Build a dictionary of brush solids This will help with future conditions and skybox generation. --- src/conditions.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/conditions.py b/src/conditions.py index eef293c45..298843d60 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -1,5 +1,7 @@ # coding: utf-8 from decimal import Decimal +from collections import namedtuple +from enum import Enum import random from utils import Vec @@ -24,6 +26,16 @@ ALL_RESULTS = [] ALL_META = [] +SOLIDS = {} # A dictionary mapping origins to their brushes +solidGroup = namedtuple('solidGroup', 'face solid normal color') + + +class MAT_TYPES(Enum): + """The values saved in the solidGroup.color attribute.""" + black = 0 + white = 1 + + xp = utils.Vec_tuple(1, 0, 0) xn = utils.Vec_tuple(-1, 0, 0) yp = utils.Vec_tuple(0, 1, 0) @@ -308,6 +320,8 @@ def init(seed, inst_list, vmf_file): # Sort by priority, where higher = done later conditions.sort() + build_solid_dict() + def check_all(): """Check all conditions.""" @@ -360,6 +374,43 @@ def check_flag(flag, inst): return res +def build_solid_dict(): + """Build a dictionary mapping origins to brush faces. + + This allows easily finding brushes that are at certain locations. + """ + import vbsp + mat_types = {} + for mat in vbsp.BLACK_PAN: + mat_types[mat] = MAT_TYPES.black + + for mat in vbsp.WHITE_PAN: + mat_types[mat] = MAT_TYPES.white + + for solid in VMF.brushes: + for face in solid: + try: + mat_type = mat_types[face.mat] + except KeyError: + continue + else: + origin = face.get_origin().as_tuple() + if origin in SOLIDS: + # The only time two textures will be in the same + # place is if they are covering each other - + # nodraw them both and ignore them + SOLIDS.pop(origin).face.mat = 'tools/toolsnodraw' + face.mat = 'tools/toolsnodraw' + continue + + SOLIDS[origin] = solidGroup( + mat_type, + face, + solid, + face.normal(), + ) + + def dump_conditions(): """Print a list of all the condition flags, results, metaconditions From f9d2bd453ae94d02863b51ddf92a4cbcf10a8ec5 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Mon, 3 Aug 2015 17:02:24 +1000 Subject: [PATCH 03/30] Add rest of PosIsSolid --- src/conditions.py | 33 +++++++++++++++++++++++++++++---- src/utils.py | 40 ++++++++++++++++++++-------------------- 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/src/conditions.py b/src/conditions.py index 4049db8cd..52525d269 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -35,6 +35,12 @@ class MAT_TYPES(Enum): black = 0 white = 1 + def __str__(self): + if self is MAT_TYPES.black: + return 'black' + if self is MAT_TYPES.white: + return 'white' + xp = utils.Vec_tuple(1, 0, 0) xn = utils.Vec_tuple(-1, 0, 0) @@ -757,7 +763,7 @@ def flag_brush_at_loc(inst, flag): """Checks to see if a wall is present at the given location. - Pos is the position of the brush, where `0 0 0` is the floor-position - of the brush, and '0 0 1' is the ceiling. + of the brush, in 16 unit increments. - Dir is the normal the face is pointing. (0 0 1) is 'up'. - Type defines the type the brush must be: - "Any" requires either a black or white brush. @@ -765,14 +771,16 @@ def flag_brush_at_loc(inst, flag): - "White" requires a portalable surface. - "Black" requires a non-portalable surface. - SetVar defines an instvar which will be given a value of "black", - 'white" or "none" to allow the result to be reused. + "white" or "none" to allow the result to be reused. - RemoveBrush: If set to 1, the brush will be removed if found. + Only do this to EmbedFace brushes, since it will remove the other + sides as well. """ pos = Vec.from_str(inst['origin', '0 0 0']) # Relative to the instance origin pos += ( Vec.from_str(flag['pos', '0 0 0']) - * 128 # 128 units between blocks + * 16 # 128 units between blocks - (0, 0, 64) # Subtract so origin is the floor-position ).rotate_by_str( inst['angles', '0 0 0'] @@ -783,10 +791,27 @@ def flag_brush_at_loc(inst, flag): ) result_var = flag['setVar', ''] - should_remove = utils.conv_bool(flag['RemoveBrush']) + should_remove = utils.conv_bool(flag['RemoveBrush'], False) des_type = flag['type', 'any'].casefold() + brush = SOLIDS.get(pos.as_tuple(), None) + ':type brush: solidGroup' + if brush is None or brush.normal != norm: + br_type = 'none' + else: + br_type = str(brush.color) + if should_remove: + VMF.remove_brush( + brush.solid, + ) + + if result_var: + inst.fixup[result_var] = br_type + + if des_type == 'any' and br_type != 'none': + return True + return des_type == br_type ########### diff --git a/src/utils.py b/src/utils.py index 3bd8025b3..5b8fbc4c8 100644 --- a/src/utils.py +++ b/src/utils.py @@ -549,7 +549,7 @@ def rotate_by_str(self, ang, pitch=0.0, yaw=0.0, roll=0.0, round_vals=True): round_vals, ) - def __add__(self, other): + def __add__(self, other) -> 'Vec': """+ operation. This additionally works on scalars (adds to all axes). @@ -560,7 +560,7 @@ def __add__(self, other): return Vec(self.x + other, self.y + other, self.z + other) __radd__ = __add__ - def __sub__(self, other): + def __sub__(self, other) -> 'Vec': """- operation. This additionally works on scalars (adds to all axes). @@ -581,7 +581,7 @@ def __sub__(self, other): except TypeError: return NotImplemented - def __rsub__(self, other): + def __rsub__(self, other) -> 'Vec': """- operation. This additionally works on scalars (adds to all axes). @@ -602,7 +602,7 @@ def __rsub__(self, other): except TypeError: return NotImplemented - def __mul__(self, other): + def __mul__(self, other) -> 'Vec': """Multiply the Vector by a scalar.""" if isinstance(other, Vec): return NotImplemented @@ -617,7 +617,7 @@ def __mul__(self, other): return NotImplemented __rmul__ = __mul__ - def __div__(self, other): + def __div__(self, other) -> 'Vec': """Divide the Vector by a scalar. If any axis is equal to zero, it will be kept as zero as long @@ -635,7 +635,7 @@ def __div__(self, other): except TypeError: return NotImplemented - def __rdiv__(self, other): + def __rdiv__(self, other) -> 'Vec': """Divide a scalar by a Vector. """ @@ -651,7 +651,7 @@ def __rdiv__(self, other): except TypeError: return NotImplemented - def __floordiv__(self, other): + def __floordiv__(self, other) -> 'Vec': """Divide the Vector by a scalar, discarding the remainder. If any axis is equal to zero, it will be kept as zero as long @@ -669,7 +669,7 @@ def __floordiv__(self, other): except TypeError: return NotImplemented - def __mod__(self, other): + def __mod__(self, other) -> 'Vec': """Compute the remainder of the Vector divided by a scalar.""" if isinstance(other, Vec): return NotImplemented @@ -683,7 +683,7 @@ def __mod__(self, other): except TypeError: return NotImplemented - def __divmod__(self, other): + def __divmod__(self, other) -> ('Vec', 'Vec'): """Divide the vector by a scalar, returning the result and remainder. """ @@ -699,7 +699,7 @@ def __divmod__(self, other): else: return Vec(x1, y1, z1), Vec(x2, y2, z2) - def __iadd__(self, other): + def __iadd__(self, other) -> 'Vec': """+= operation. Like the normal one except without duplication. @@ -722,7 +722,7 @@ def __iadd__(self, other): ) from e return self - def __isub__(self, other): + def __isub__(self, other) -> 'Vec': """-= operation. Like the normal one except without duplication. @@ -745,7 +745,7 @@ def __isub__(self, other): ) from e return self - def __imul__(self, other): + def __imul__(self, other) -> 'Vec': """*= operation. Like the normal one except without duplication. @@ -758,7 +758,7 @@ def __imul__(self, other): self.z *= other return self - def __idiv__(self, other): + def __idiv__(self, other) -> 'Vec': """/= operation. Like the normal one except without duplication. @@ -771,7 +771,7 @@ def __idiv__(self, other): self.z /= other return self - def __ifloordiv__(self, other): + def __ifloordiv__(self, other) -> 'Vec': """//= operation. Like the normal one except without duplication. @@ -784,7 +784,7 @@ def __ifloordiv__(self, other): self.z //= other return self - def __imod__(self, other): + def __imod__(self, other) -> 'Vec': """%= operation. Like the normal one except without duplication. @@ -797,11 +797,11 @@ def __imod__(self, other): self.z %= other return self - def __bool__(self): + def __bool__(self) -> bool: """Vectors are True if any axis is non-zero.""" return self.x != 0 or self.y != 0 or self.z != 0 - def __eq__(self, other): + def __eq__(self, other) -> bool: """== test. Two Vectors are compared based on the axes. @@ -822,7 +822,7 @@ def __eq__(self, other): except ValueError: return NotImplemented - def __lt__(self, other): + def __lt__(self, other) -> bool: """A bool: """A<=B test. Two Vectors are compared based on the axes. @@ -872,7 +872,7 @@ def __le__(self, other): except ValueError: return NotImplemented - def __gt__(self, other): + def __gt__(self, other) -> bool: """A>B test. Two Vectors are compared based on the axes. From 259f8b27b6cf5c2a441720e63ca39f8d2be026f1 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Mon, 3 Aug 2015 18:04:45 +1000 Subject: [PATCH 04/30] Then make posIsSolid work. --- src/conditions.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/conditions.py b/src/conditions.py index 52525d269..7d53264a8 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -410,10 +410,10 @@ def build_solid_dict(): continue SOLIDS[origin] = solidGroup( - mat_type, - face, - solid, - face.normal(), + color=mat_type, + face=face, + solid=solid, + normal=face.normal(), ) @@ -764,7 +764,7 @@ def flag_brush_at_loc(inst, flag): - Pos is the position of the brush, where `0 0 0` is the floor-position of the brush, in 16 unit increments. - - Dir is the normal the face is pointing. (0 0 1) is 'up'. + - Dir is the normal the face is pointing. (0 0 -1) is 'up'. - Type defines the type the brush must be: - "Any" requires either a black or white brush. - "None" means that no brush must be present. @@ -776,17 +776,15 @@ def flag_brush_at_loc(inst, flag): Only do this to EmbedFace brushes, since it will remove the other sides as well. """ - pos = Vec.from_str(inst['origin', '0 0 0']) + pos = Vec.from_str(flag['pos', '0 0 0']) + pos *= 16 # 16 per quarter-tile + pos.z -= 64 # Subtract so origin is the floor-position + pos = pos.rotate_by_str(inst['angles', '0 0 0']) + # Relative to the instance origin - pos += ( - Vec.from_str(flag['pos', '0 0 0']) - * 16 # 128 units between blocks - - (0, 0, 64) # Subtract so origin is the floor-position - ).rotate_by_str( - inst['angles', '0 0 0'] - ) + pos += Vec.from_str(inst['origin', '0 0 0']) - norm = Vec.from_str(flag['dir', '0 0 1']).rotate_by_str( + norm = Vec.from_str(flag['dir', '0 0 -1']).rotate_by_str( inst['angles', '0 0 0'] ) @@ -796,6 +794,7 @@ def flag_brush_at_loc(inst, flag): brush = SOLIDS.get(pos.as_tuple(), None) ':type brush: solidGroup' + if brush is None or brush.normal != norm: br_type = 'none' else: From 6db480c314d4055928dc0dcb024a2d48dcad8c63 Mon Sep 17 00:00:00 2001 From: Spencer Brown Date: Mon, 3 Aug 2015 18:35:21 +1000 Subject: [PATCH 05/30] Change location of styled vmfs OSX vbsp does not handle VMFs in subdirectories, so it must be placed in the regular maps/ directory. --- src/vbsp.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/vbsp.py b/src/vbsp.py index 7ecc4f80d..f5c3c3d64 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -1862,9 +1862,11 @@ def main(): conditions.dump_conditions() sys.exit() - # Add styled/ to the list of directories for the new location - path_dir, path_file = os.path.split(path) - new_args[-1] = new_path = os.path.join(path_dir, 'styled', path_file) + if not path.endswith(".vmf"): + path += ".vmf" + + # Append styled to the map path. + new_path = path[:-4] + '_styled.vmf' for i, a in enumerate(new_args): # We need to strip these out, otherwise VBSP will get confused. @@ -1880,9 +1882,6 @@ def main(): utils.con_log('Map path is "' + path + '"') if path == "": raise Exception("No map passed!") - if not path.endswith(".vmf"): - path += ".vmf" - new_path += ".vmf" if '-force_peti' in args or '-force_hammer' in args: # we have override command! From 24d7aa53fa7b45659532601a649357ab7542d411 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Mon, 3 Aug 2015 18:42:08 +1000 Subject: [PATCH 06/30] Make the changed location actually work. --- src/vbsp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vbsp.py b/src/vbsp.py index f5c3c3d64..60b4ff637 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -1866,7 +1866,7 @@ def main(): path += ".vmf" # Append styled to the map path. - new_path = path[:-4] + '_styled.vmf' + new_path = new_args[-1] = path[:-4] + '_styled.vmf' for i, a in enumerate(new_args): # We need to strip these out, otherwise VBSP will get confused. From 3f94f1215d4e4647860382dfd03ac2398bbabcf0 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 6 Aug 2015 16:42:29 +1000 Subject: [PATCH 07/30] Allow tooltip.add_tooltip() text to be dynamic Also remove the _tooltip_id from widgets, we can use nonlocal instead. --- src/contextWin.py | 5 +++-- src/tooltip.py | 33 +++++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/contextWin.py b/src/contextWin.py index 34f60e91f..1d2a613ab 100644 --- a/src/contextWin.py +++ b/src/contextWin.py @@ -221,6 +221,8 @@ def load_item_data(): wid['moreinfo'].state(['disabled']) else: wid['moreinfo'].state(['!disabled']) + wid['moreinfo'].tooltip_text = selected_item.url + editor_data = item_data['editor'] has_inputs = False has_polarity = False @@ -417,8 +419,7 @@ def show_more_info(): wid['moreinfo'] = ttk.Button(f, text="More Info>>", command=show_more_info) wid['moreinfo'].grid(row=6, column=2, sticky=E) - wid['moreinfo'].bind('', more_info_show_url) - wid['moreinfo'].bind('', tooltip.hide) + tooltip.add_tooltip(wid['moreinfo']) menu_info = Menu(wid['moreinfo']) menu_info.add_command(label='', state='disabled') diff --git a/src/tooltip.py b/src/tooltip.py index 66d8e748d..1f51a8499 100644 --- a/src/tooltip.py +++ b/src/tooltip.py @@ -65,28 +65,41 @@ def hide(e=None): window.withdraw() -def add_tooltip(targ_widget, text, delay=500): - """Add a tooltip to the specified widget.""" +def add_tooltip(targ_widget, text='', delay=500): + """Add a tooltip to the specified widget. + + delay is the amount of milliseconds of hovering needed to show the + tooltip. + text is the initial text for the tooltip. + Set targ_widget.tooltip_text to change the tooltip dynamically. + """ + targ_widget.tooltip_text = text + event_id = None # The id of the enter event, so we can cancel it. def after_complete(): """Remove the id and show the tooltip after the delay.""" - del targ_widget._tooltip_id - show(targ_widget, text) + nonlocal event_id + event_id = None # Invalidate event id + if targ_widget.tooltip_text: + show(targ_widget, targ_widget.tooltip_text) def enter_handler(e): """Schedule showing the tooltip.""" - targ_widget._tooltip_id = TK_ROOT.after( - delay, - after_complete, - ) + nonlocal event_id + if targ_widget.tooltip_text: + event_id = TK_ROOT.after( + delay, + after_complete, + ) def exit_handler(e): """When the user leaves, cancel the event.""" # We only want to cancel if the event hasn't expired already + nonlocal event_id hide() - if hasattr(targ_widget, '_tooltip_id'): + if event_id is not None: TK_ROOT.after_cancel( - targ_widget._tooltip_id + event_id ) targ_widget.bind( From eb1e73614f4e9629c9ee3544f91dfe2e43276874 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 6 Aug 2015 17:07:21 +1000 Subject: [PATCH 08/30] Add tooltips for BEE2 options --- src/UI.py | 2 +- src/optionWindow.py | 52 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/UI.py b/src/UI.py index b52f85e3c..8c9b13de2 100644 --- a/src/UI.py +++ b/src/UI.py @@ -1808,7 +1808,7 @@ def init_windows(): init_drag_icon() loader.step('UI') - optionWindow.reset_all_win = reposition_panes + optionWindow.reset_all_win = reset_panes TK_ROOT.deiconify() # show it once we've loaded everything diff --git a/src/optionWindow.py b/src/optionWindow.py index c34307495..c50a3ee9b 100644 --- a/src/optionWindow.py +++ b/src/optionWindow.py @@ -4,6 +4,7 @@ from tk_root import TK_ROOT from BEE2_config import GEN_OPTS +from tooltip import add_tooltip import sound import utils @@ -17,7 +18,12 @@ VARS = {} + def reset_all_win(): + """Return all windows to their default positions. + + This is replaced by `UI.reset_panes`. + """ pass win = Toplevel(TK_ROOT) @@ -26,16 +32,19 @@ def reset_all_win(): win.title('BEE2 Options') win.withdraw() + def show(): win.deiconify() contextWin.hide_context() # Ensure this closes utils.center_win(win) + def load(): """Load the current settings from config.""" for var in VARS.values(): var.load() + def save(): """Save settings into the config and apply them to other windows.""" for var in VARS.values(): @@ -47,7 +56,16 @@ def save(): for func in refresh_callbacks: func() -def make_checkbox(frame, section, item, desc, default=False, var=None): + +def make_checkbox( + frame, + section, + item, + desc, + default=False, + var: BooleanVar=None, + tooltip='', + ): """Add a checkbox to the given frame which toggles an option. section and item are the location in GEN_OPTS for this config. @@ -87,6 +105,10 @@ def load_opt(): variable=var, text=desc, ) + + if tooltip: + add_tooltip(widget, tooltip) + UI[section, item] = widget return widget @@ -160,6 +182,7 @@ def cancel(): save() # And ensure they are applied to other windows + def init_gen_tab(f): if sound.initiallised: @@ -176,6 +199,11 @@ def init_gen_tab(f): text='Play Sounds', state='disabled', ) + add_tooltip( + UI['mute'], + 'PyGame is either not installed or broken.\n' + 'Sound effects have been disabled.' + ) mute.grid(row=0, column=0, sticky=W) make_checkbox( @@ -183,6 +211,8 @@ def init_gen_tab(f): section='General', item='show_wip_items', desc='Show WIP items', + tooltip='Show items and item versions marked Work In Progress. ' + 'These may be buggy or incomplete.', var=SHOW_WIP, ).grid(row=1, column=0, sticky=W) @@ -192,8 +222,9 @@ def init_win_tab(f): f, section='General', item='keep_win_inside', - desc='Keep windows inside screen \n' - '(disable for multi-monitor setups)', + desc='Keep windows inside screen', + tooltip='Allow sub-windows to move outside the screen borders. ' + 'If you have multiple monitors, disable this.', var=KEEP_WIN_INSIDE, ) keep_inside.grid(row=0, column=0, sticky=W) @@ -206,6 +237,7 @@ def init_win_tab(f): ) reset_win.grid(row=1, column=0, sticky=EW) + def init_dev_tab(f): f.columnconfigure(1, weight=1) f.columnconfigure(2, weight=1) @@ -215,6 +247,8 @@ def init_dev_tab(f): section='Debug', item='log_missing_ent_count', desc='Log missing entity counts', + tooltip='When loading items, log items with missing entity counts ' + 'in their properties.txt file.', ).grid(row=0, column=0, sticky=W) make_checkbox( @@ -222,6 +256,8 @@ def init_dev_tab(f): section='Debug', item='log_missing_styles', desc="Log when item doesn't have a style", + tooltip='Log items have no applicable version for a particular style.' + 'This usually means it will look very bad.', ).grid(row=1, column=0, sticky=W) make_checkbox( @@ -229,6 +265,9 @@ def init_dev_tab(f): section='Debug', item='log_item_fallbacks', desc="Log when item uses parent's style", + tooltip='Log when an item reuses a variant from a parent style ' + '(1970s using 1950s items, for example). This is usually ' + 'fine, but may need to be fixed.', ).grid(row=3, column=0, sticky=W) make_checkbox( @@ -236,6 +275,8 @@ def init_dev_tab(f): section='Debug', item='show_errors', desc="Show detailed error message", + tooltip='If an error occurs, show the error and traceback ' + 'before quitting.', ).grid(row=0, column=1, sticky=W) make_checkbox( @@ -243,4 +284,9 @@ def init_dev_tab(f): section='General', item='preserve_bee2_resource_dir', desc='Preserve Game Directories', + tooltip='When exporting, do not overwrite \n"bee2/" and' + '\n"sdk_content/maps/bee2/".\n' + 'Enable if you\'re' + ' developing new content, to ensure it is not ' + 'overwritten.', ).grid(row=1, column=1, sticky=W) \ No newline at end of file From ef89bf93636ed4147cbc75550ebf1d89d6b354be Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 6 Aug 2015 18:55:22 +1000 Subject: [PATCH 09/30] Allow splitting up descriptions for an item between the style variants and generally This will decrease the current huge amounts of description duplications. --- src/contextWin.py | 29 ++++++++++++++++++++++++++++- src/packageLoader.py | 19 ++++++++++++++----- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/contextWin.py b/src/contextWin.py index 1d2a613ab..6fa5906e1 100644 --- a/src/contextWin.py +++ b/src/contextWin.py @@ -11,6 +11,7 @@ from tk_root import TK_ROOT from tkinter import ttk from tkinter import messagebox + import functools import webbrowser @@ -164,6 +165,26 @@ def set_item_version(_=None): load_item_data() +def get_description(global_last, glob_desc, style_desc): + """Join together the general and style description for an item.""" + if glob_desc and style_desc: + # We have both, we need to join them together. + if global_last: + yield from style_desc + yield (('line', '')) + yield from glob_desc + else: + yield from glob_desc + yield (('line', '')) + yield from style_desc + elif glob_desc: + yield from glob_desc + elif style_desc: + yield from style_desc + else: + return # No description + + def load_item_data(): """Refresh the window to use the selected item's data.""" global version_lookup @@ -182,7 +203,13 @@ def load_item_data(): wid['name']['text'] = selected_sub_item.name wid['ent_count']['text'] = item_data['ent'] - wid['desc'].set_text(item_data['desc']) + wid['desc'].set_text( + get_description( + global_last=selected_item.item.glob_desc_last, + glob_desc=selected_item.item.glob_desc, + style_desc=item_data['desc'] + ) + ) if itemPropWin.can_edit(selected_item.properties()): wid['changedefaults'].state(['!disabled']) diff --git a/src/packageLoader.py b/src/packageLoader.py index c9fd07d7a..02053b507 100644 --- a/src/packageLoader.py +++ b/src/packageLoader.py @@ -521,6 +521,8 @@ def __init__( needs_unlock=False, all_conf=None, unstyled=False, + glob_desc=(), + desc_last=False ): self.id = item_id self.versions = versions @@ -529,6 +531,8 @@ def __init__( self.needs_unlock = needs_unlock self.all_conf = all_conf or Property(None, []) self.unstyled = unstyled + self.glob_desc = glob_desc + self.glob_desc_last = desc_last @classmethod def parse(cls, data): @@ -538,6 +542,9 @@ def parse(cls, data): folders = {} unstyled = utils.conv_bool(data.info['unstyled', '0']) + glob_desc = list(desc_parse(data.info)) + desc_last = utils.conv_bool(data.info['AllDescLast', '0']) + all_config = get_config( data.info, data.zip_file, @@ -580,11 +587,13 @@ def parse(cls, data): return cls( data.id, - versions, - def_version, - needs_unlock, - all_config, - unstyled, + versions=versions, + def_version=def_version, + needs_unlock=needs_unlock, + all_conf=all_config, + unstyled=unstyled, + glob_desc=glob_desc, + desc_last=desc_last, ) def add_over(self, override): From 2717f89500c4377e53d153708718d4e2b2932dad Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sat, 8 Aug 2015 17:39:44 +1000 Subject: [PATCH 10/30] Separate fizzler thickness-changing out, and allow it to be used in CustFizzler. --- src/conditions.py | 66 +++++++++++++++++++++++++++----- src/vbsp.py | 96 ++++++++++++++++++++++++++++------------------- 2 files changed, 114 insertions(+), 48 deletions(-) diff --git a/src/conditions.py b/src/conditions.py index 7d53264a8..7c2606b2f 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -517,6 +517,42 @@ def add_suffix(inst, suff): inst['file'] = ''.join((old_name, suff, dot, ext)) +def widen_fizz_brush(brush, thickness, bounds=None): + """Move the two faces of a fizzler brush outward. + + This is good to make fizzlers which are thicker than 2 units. + bounds is the output of .get_bbox(), if this should be overriden + """ + + # Subtract 2 for the fizzler width, and divide + # to get the difference for each face. + offset = (thickness-2)/2 + + if bounds is None: + bound_min, bound_max = brush.get_bbox() + else: + # Allow passing these in + bound_min, bound_max = bounds + origin = (bound_max + bound_min) / 2 # type: Vec + size = bound_max - bound_min + for axis in 'xyz': + # One of the directions will be thinner than 128, that's the fizzler + # direction. + if size[axis] < 128: + bound_min[axis] -= offset + bound_max[axis] += offset + + for face in brush: + # For every coordinate, set to the maximum if it's larger than the + # origin. This will expand the two sides. + for v in face.planes: + for axis in 'xyz': + if v[axis] > origin[axis]: + v[axis] = bound_max[axis] + else: + v[axis] = bound_min[axis] + + @make_flag('debug') def debug_flag(inst, props): """Displays text when executed, for debugging conditions. @@ -1161,20 +1197,22 @@ def res_cust_fizzler(base_inst, res): This should be executed on the base instance. Brush and MakeLaserField are ignored on laserfield barriers. Options: - - ModelName: sets the targetname given to the model instances. - - UniqueModel: If true, each model instance will get a suffix to + * ModelName: sets the targetname given to the model instances. + * UniqueModel: If true, each model instance will get a suffix to allow unique targetnames. - - Brush: A brush entity that will be generated (the original is + * Brush: A brush entity that will be generated (the original is deleted.) - - Name is the instance name for the brush - - Left/Right/Center/Short/Nodraw are the textures used - - Keys are a block of keyvalues to be set. Targetname and + * Name is the instance name for the brush + * Left/Right/Center/Short/Nodraw are the textures used + * Keys are a block of keyvalues to be set. Targetname and Origin are auto-set. - - MakeLaserField generates a brush stretched across the whole + * Thickness will change the thickness of the fizzler if set. + By default it is 2 units thick. + * MakeLaserField generates a brush stretched across the whole area. - - Name and keys are the same as the regular Brush. - - Texture/Nodraw are the textures. - - Width is the pixel width of the laser texture, used to + * Name, keys and thickness are the same as the regular Brush. + * Texture/Nodraw are the textures. + * Width is the pixel width of the laser texture, used to scale it correctly. """ from vbsp import TEX_FIZZLER @@ -1284,6 +1322,14 @@ def res_cust_fizzler(base_inst, res): # If we fail, just use the original textures pass + widen_amount = utils.conv_float(config['thickness', '2'], 2.0) + if widen_amount != 2: + for brush in new_brush.solids: + widen_fizz_brush( + brush, + thickness=widen_amount, + ) + def convert_to_laserfield( brush: VLib.Entity, diff --git a/src/vbsp.py b/src/vbsp.py index 60b4ff637..564b87dfa 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -448,6 +448,12 @@ def static_pan(inst): # white/black are found via the func_brush make_static_pan(inst, "glass") + + +FIZZ_BUMPER_WIDTH = 32 # The width of bumper brushes +FIZZ_NOPORTAL_WIDTH = 16 # Width of noportal_volumes + + @conditions.meta_cond(priority=200, only_once=True) def anti_fizz_bump(inst): """Create portal_bumpers and noportal_volumes surrounding fizzlers. @@ -456,11 +462,15 @@ def anti_fizz_bump(inst): It is only applied to trigger_portal_cleansers with the Client flag checked. """ - FIZZ_OFF_WIDTH = 16 - 1 # We extend 15 units on each side, - # giving 32 in total: the width of a fizzler model. + # Subtract 2 for the fizzler width, and divide + # to get the difference for each face. + if not utils.conv_bool(settings['style_vars']['fixfizzlerbump']): return True + # Only use 1 bumper entity for each fizzler, since we can. + bumpers = {} + utils.con_log('Adding Portal Bumpers to fizzlers...') for cleanser in VMF.by_class['trigger_portal_cleanser']: # Client bit flag = 1, triggers without it won't destroy portals @@ -473,54 +483,58 @@ def anti_fizz_bump(inst): # Fizzlers will be changed to this in fix_func_brush() fizz_name = fizz_name[:-6] + '-br_brush' - utils.con_log('name:', fizz_name) - # We can't combine the bumpers, since noportal_volumes - # don't work with concave areas - bumper = VMF.create_ent( - classname='func_portal_bumper', + # Only have 1 bumper per brush + if fizz_name not in bumpers: + bumper = bumpers[fizz_name] = VMF.create_ent( + classname='func_portal_bumper', + targetname=fizz_name, + origin=cleanser['origin'], + spawnflags='1', + # Start off, we can't really check if the original + # does, but that's usually handled by the instance anyway. + ) + else: + bumper = bumpers[fizz_name] + + # Noportal_volumes need separate parts, since they can't be + # concave. + noportal = VMF.create_ent( + classname='func_noportal_volume', targetname=fizz_name, origin=cleanser['origin'], spawnflags='1', - # Start off, we can't really check if the original - # does, but that's usually handled by the instance anyway. ) - bound_min, bound_max = cleanser.get_bbox() - origin = (bound_max + bound_min) / 2 # type: Vec - size = bound_max - bound_min - for axis in 'xyz': - # One of the directions will be thinner than 128, that's the fizzler - # direction. - if size[axis] < 128: - bound_max[axis] += FIZZ_OFF_WIDTH - bound_min[axis] -= FIZZ_OFF_WIDTH - break - # Copy one of the solids to use as a base, so the texture axes # are correct. if len(cleanser.solids) == 1: # It's a 128x128 brush, with only one solid - new_solid = cleanser.solids[0].copy() + bumper_brush = cleanser.solids[0].copy() else: # It's a regular one, we want the middle/large section - new_solid = cleanser.solids[1].copy() - bumper.solids.append(new_solid) + bumper_brush = cleanser.solids[1].copy() + bumper.solids.append(bumper_brush) + + noportal_brush = bumper_brush.copy() + noportal.solids.append(noportal_brush) - for face in new_solid: + conditions.widen_fizz_brush( + bumper_brush, + FIZZ_BUMPER_WIDTH, + bounds=cleanser.get_bbox(), + ) + + conditions.widen_fizz_brush( + noportal_brush, + FIZZ_NOPORTAL_WIDTH, + bounds=cleanser.get_bbox(), + ) + + for face in bumper_brush: face.mat = 'tools/toolsinvisible' - # For every coordinate, set to the maximum if it's larger than the - # origin. This will expand the two sides. - for v in face.planes: - for axis in 'xyz': - if v[axis] > origin[axis]: - v[axis] = bound_max[axis] - else: - v[axis] = bound_min[axis] - noportal = bumper.copy() - # Add a noportal_volume as well, of the same size. - noportal['classname'] = 'func_noportal_volume' - VMF.add_ent(noportal) + for face in noportal_brush: + face.mat = 'tools/toolsinvisible' utils.con_log('Done!') @@ -1865,8 +1879,13 @@ def main(): if not path.endswith(".vmf"): path += ".vmf" - # Append styled to the map path. - new_path = new_args[-1] = path[:-4] + '_styled.vmf' + # Append styled/ to the directory path. + path_dir, path_file = os.path.split(path) + new_path = new_args[-1] = os.path.join( + path_dir, + 'styled', + path_file, + ) for i, a in enumerate(new_args): # We need to strip these out, otherwise VBSP will get confused. @@ -1880,6 +1899,7 @@ def main(): new_args[i+1] = '' utils.con_log('Map path is "' + path + '"') + utils.con_log('New path: "' + new_path + '"') if path == "": raise Exception("No map passed!") From c8e6baffb901773ba6ccdec7dc168beef604bdc2 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Fri, 14 Aug 2015 19:03:36 +1000 Subject: [PATCH 11/30] Add tooltip to 'Change Defaults' button Also prevent a tooltip from showing if the widget is disabled. --- src/contextWin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/contextWin.py b/src/contextWin.py index 6fa5906e1..ecbc15694 100644 --- a/src/contextWin.py +++ b/src/contextWin.py @@ -465,6 +465,10 @@ def show_item_props(): command=show_item_props, ) wid['changedefaults'].grid(row=6, column=1) + tooltip.add_tooltip( + wid['changedefaults'], + 'Change the default settings for this item when placed.' + ) wid['variant'] = ttk.Combobox(f, values=['VERSION'], exportselection=0) wid['variant'].state(['readonly']) # Prevent directly typing in values From 32fa63afc8429624a82320255736cf0f09053174 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Fri, 14 Aug 2015 19:03:53 +1000 Subject: [PATCH 12/30] Prevent tooltip from showing if widget is disabled --- src/tooltip.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tooltip.py b/src/tooltip.py index 1f51a8499..b99650bb9 100644 --- a/src/tooltip.py +++ b/src/tooltip.py @@ -72,6 +72,7 @@ def add_tooltip(targ_widget, text='', delay=500): tooltip. text is the initial text for the tooltip. Set targ_widget.tooltip_text to change the tooltip dynamically. + If the target widget is disabled, no context menu will be shown. """ targ_widget.tooltip_text = text event_id = None # The id of the enter event, so we can cancel it. @@ -87,6 +88,9 @@ def enter_handler(e): """Schedule showing the tooltip.""" nonlocal event_id if targ_widget.tooltip_text: + if hasattr(targ_widget, 'instate'): + if not targ_widget.instate(('!disabled',)): + return event_id = TK_ROOT.after( delay, after_complete, From 47db5c0c64d433cc2f78cbce0f2aea3c9d956e2a Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Fri, 14 Aug 2015 19:04:31 +1000 Subject: [PATCH 13/30] Don't use ../ when extracting This might stop a bug on some systems. --- src/extract_packages.py | 5 +++-- src/packageLoader.py | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/extract_packages.py b/src/extract_packages.py index 7440169cc..4715786b3 100644 --- a/src/extract_packages.py +++ b/src/extract_packages.py @@ -29,7 +29,8 @@ def done_callback(): def do_copy(zip_list, done_files): - shutil.rmtree('../cache/', ignore_errors=True) + cache_path = os.path.abspath('../cache/') + shutil.rmtree(cache_path, ignore_errors=True) img_loc = os.path.join('resources', 'bee2') for zip_path in zip_list: @@ -43,7 +44,7 @@ def do_copy(zip_list, done_files): if loc.startswith("resources"): # Don't re-extract images if not loc.startswith(img_loc): - zip_file.extract(path, path="../cache/") + zip_file.extract(path, path=cache_path) with currently_done.get_lock(): done_files.value += 1 diff --git a/src/packageLoader.py b/src/packageLoader.py index 02053b507..8d78a772a 100644 --- a/src/packageLoader.py +++ b/src/packageLoader.py @@ -200,14 +200,16 @@ def load_packages( data[obj_type].append(object_) loader.step("OBJ") - shutil.rmtree('../cache/', ignore_errors=True) + cache_folder = os.path.abspath('../cache/') + + shutil.rmtree(cache_folder, ignore_errors=True) img_loc = os.path.join('resources', 'bee2') for zip_file in zips: for path in zip_names(zip_file): loc = os.path.normcase(path).casefold() if loc.startswith(img_loc): loader.step("IMG_EX") - zip_file.extract(path, path="../cache/") + zip_file.extract(path, path=cache_folder) shutil.rmtree('../images/cache', ignore_errors=True) if os.path.isdir("../cache/resources/bee2"): From 3094455d2a28184568e1fc69f2e18a60558bebd4 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Wed, 19 Aug 2015 19:15:43 +1000 Subject: [PATCH 14/30] Display nice usage message when VBSP is called with no args. --- src/vbsp.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/vbsp.py b/src/vbsp.py index 564b87dfa..e6d7cf09b 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -1871,6 +1871,21 @@ def main(): old_args = sys.argv[1:] path = sys.argv[-1] # The path is the last argument to vbsp + if not old_args: + # No arguments! + utils.con_log( + 'No arguments!\n' + "The BEE2 VBSP takes all the regular VBSP's " + 'arguments, with some extra arguments:\n' + '-dump_conditions: Print a list of all condition flags,\n' + ' results, and metaconditions.\n' + '-force_peti: Force enabling map conversion. \n' + "-force_hammer: Don't convert the map at all.\n" + '-entity_limit: A default VBSP command, this is inspected to' + 'determine if the map is PeTI or not.' + ) + sys.exit() + if old_args[0].casefold() == '-dump_conditions': # Print all the condition flags, results, and metaconditions conditions.dump_conditions() From 87331bb6fcec2a1e27a0ceecd6691bca1e3b87d5 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Wed, 19 Aug 2015 19:20:46 +1000 Subject: [PATCH 15/30] Various speed optimisations - Get rid of Property.valid functionality (never used, just slows down code) - Don't provide lots of options to generate property trees - Use a set instead of a list for ent/solid ID management - Just check to see if a keyvalue starts with 'replace', instead of looping over a list - Don't build a dictionary for every Face's initialisation --- src/property_parser.py | 151 ++++++++++++++--------------------- src/utils.py | 19 +++-- src/vmfLib.py | 173 +++++++++++++++++++++++------------------ 3 files changed, 168 insertions(+), 175 deletions(-) diff --git a/src/property_parser.py b/src/property_parser.py index 2cfe54967..1210d28c6 100644 --- a/src/property_parser.py +++ b/src/property_parser.py @@ -17,8 +17,6 @@ r'\/': '/', } -INVALID = object() - # Sentinel value to indicate that no default was given to find_key() _NO_KEY_FOUND = object() @@ -77,53 +75,39 @@ def __str__(self): class Property: - """Represents Property found in property files, like those used by Valve.""" + """Represents Property found in property files, like those used by Valve. + + Value should be a string (for leaf properties), or a list of children + Property objects. + The name should be a string, or None for a root object. + Root objects export each child at the topmost indent level. + This is produced from Property.parse() calls. + + :type value: list | str + :type name: str | None + :type _folded_name: str | None + :type real_name: str | None + """ # Helps decrease memory footprint with lots of Property values. - __slots__ = ('_folded_name', 'real_name', 'value', 'valid') + __slots__ = ('_folded_name', 'real_name', 'value') - def __init__(self, name, *values, **kargs): + def __init__(self, name, value=''): """Create a new property instance. - Values can be passed in 4 ways: - - A single value for the Property - - A number of Property objects for the tree - - A set of keyword arguments which will be converted into - Property objects - - A single dictionary which will be converted into Property - objects - Values default to just ''. - If INVALID is passed as the only parameter, an Property object - will be returned that has be marked as invalid. - If the name is set to None, this is a root Property object - it - exports each of its children at the top-most indent level. - This is produced from Property.parse() calls. + """ - if name == INVALID: - self.real_name = None - self._folded_name = None - self.value = None - self.valid = False - else: - self.name = name - if len(values) == 1: - if isinstance(values[0], Property): - self.value = [values[0]] - elif isinstance(values[0], dict): - self.value = [Property(key, val) for key, val in values[0].items()] - else: - self.value = values[0] - else: - self.value = list(values) - self.value.extend(Property(key, val) for key, val in kargs.items()) - if values == 0 and len(kargs) == 0: - self.value = '' - self.valid = True + self.real_name = name + self.value = value + self._folded_name = ( + None if name is None + else name.casefold() + ) @property def name(self): """Name automatically casefolds() any given names. This ensures comparisons are always case-sensitive. - Read .real_name to get the original value + Read .real_name to get the original value. """ return self._folded_name @@ -134,7 +118,6 @@ def name(self, new_name): self._folded_name = None else: self._folded_name = new_name.casefold() - pass def edit(self, name=None, value=None): """Simultaneously modify the name and value.""" @@ -152,7 +135,6 @@ def parse(file_contents, filename='') -> "Property": file_contents should be an iterable of strings """ open_properties = [Property(None, [])] - for line_num, line in enumerate(file_contents, start=1): values = open_properties[-1].value freshline = utils.clean_line(line) @@ -183,10 +165,6 @@ def parse(file_contents, filename='') -> "Property": value = None values.append(Property(name, value)) - # handle name bare on one line, will need a brace on - # the next line - elif utils.is_identifier(freshline): - values.append(Property(freshline, [])) elif freshline.startswith('{'): if values[-1].value: raise KeyValError( @@ -199,6 +177,10 @@ def parse(file_contents, filename='') -> "Property": open_properties.append(values[-1]) elif freshline.startswith('}'): open_properties.pop() + # handle name bare on one line, will need a brace on + # the next line + elif utils.is_identifier(freshline): + values.append(Property(freshline, [])) else: raise KeyValError( "Unexpected beginning character '" @@ -252,7 +234,7 @@ def find_key(self, key, def_=_NO_KEY_FOUND) -> 'Property': """ key = key.casefold() for prop in reversed(self.value): - if prop.name is not None and prop.name == key: + if prop.name == key: return prop if def_ is _NO_KEY_FOUND: raise NoKeyError(key) @@ -317,16 +299,11 @@ def as_dict(self): else: return self.value - def make_invalid(self): - """Soft delete this property tree, so it does not appear in any output. + def __eq__(self, other): + """Compare two items and determine if they are equal. + This ignores names. """ - self.valid = False - self.value = None # Dump this if it exists - self.name = None - - def __eq__(self, other): - """Compare two items and determine if they are equal. This ignores names.""" if isinstance(other, Property): return self.value == other.value else: @@ -373,24 +350,20 @@ def __len__(self): """Determine the number of child properties. Singluar Properties have a length of 1. - Invalid properties have a length of 0. """ - if self.valid: - if self.has_children(): - return len(self.value) - else: - return 1 + if self.has_children(): + return len(self.value) else: - return 0 + return 1 def __iter__(self): - """Iterate through the value list, or loop once through the single value. + """Iterate through the value list. """ if self.has_children(): - yield from self.value + return iter(self.value) else: - yield self.value + return iter((self.value,)) def __contains__(self, key): """Check to see if a name is present in the children. @@ -544,10 +517,7 @@ def has_children(self): return isinstance(self.value, list) def __repr__(self): - if self.valid: - return 'Property(' + repr(self.name) + ', ' + repr(self.value) + ')' - else: - return 'Property(INVALID)' + return 'Property(' + repr(self.name) + ', ' + repr(self.value) + ')' def __str__(self): return ''.join(self.export()) @@ -558,30 +528,25 @@ def export(self): Recursively calls itself for all child properties. If the Property is marked invalid, it will immediately return. """ - if self.valid: - out_val = '"' + str(self.real_name) + '"' - if isinstance(self.value, list): - if self.name is None: - # If the name is None, we just output the chilren - # without a "Name" { } surround. These Property - # objects represent the root. - yield from ( - line - for prop in self.value - for line in prop.export() - if prop.valid - ) - else: - yield out_val + '\n' - yield '\t{\n' - yield from ( - '\t'+line - for prop in self.value - for line in prop.export() - if prop.valid - ) - yield '\t}\n' + out_val = '"' + str(self.real_name) + '"' + if isinstance(self.value, list): + if self.name is None: + # If the name is None, we just output the chilren + # without a "Name" { } surround. These Property + # objects represent the root. + yield from ( + line + for prop in self.value + for line in prop.export() + ) else: - yield out_val + ' "' + str(self.value) + '"\n' + yield out_val + '\n' + yield '\t{\n' + yield from ( + '\t'+line + for prop in self.value + for line in prop.export() + ) + yield '\t}\n' else: - return \ No newline at end of file + yield out_val + ' "' + str(self.value) + '"\n' diff --git a/src/utils.py b/src/utils.py index 5b8fbc4c8..dda4e51e5 100644 --- a/src/utils.py +++ b/src/utils.py @@ -401,6 +401,9 @@ def __contains__(self, item): def get(self, item, default=None): return default + def __bool__(self): + return False + def __next__(self): raise StopIteration @@ -439,8 +442,15 @@ def __init__(self, x=0.0, y=0.0, z=0.0): If no value is given, that axis will be set to 0. A sequence can be passed in (as the x argument), which will use the three args as x/y/z. + :type x: int | float | Vec | list[float] """ - if isinstance(x, abc.Sequence): + if isinstance(x, (int, float)): + self.x = float(x) + self.y = float(y) + self.z = float(z) + elif isinstance(x, Vec): + self.x, self.y, self.z = x + else: try: self.x = float(x[0]) except (TypeError, KeyError): @@ -455,10 +465,7 @@ def __init__(self, x=0.0, y=0.0, z=0.0): self.z = float(x[2]) except (TypeError, KeyError): self.z = 0.0 - else: - self.x = float(x) - self.y = float(y) - self.z = float(z) + def copy(self): return Vec(self.x, self.y, self.z) @@ -1061,4 +1068,4 @@ def cross(self, other): __itruediv__ = __idiv__ abc.Mapping.register(Vec) -abc.MutableMapping.register(Vec) \ No newline at end of file +abc.MutableMapping.register(Vec) diff --git a/src/vmfLib.py b/src/vmfLib.py index 1b8965416..123d32a0c 100644 --- a/src/vmfLib.py +++ b/src/vmfLib.py @@ -5,6 +5,7 @@ import io from collections import defaultdict, namedtuple from contextlib import suppress +import itertools from property_parser import Property from utils import Vec @@ -14,15 +15,6 @@ CURRENT_HAMMER_VERSION = 400 CURRENT_HAMMER_BUILD = 5304 - -# According to VBSP code, fixups don't appear to have a size limit -# More than 50 shouldn't be needed, since Hammer only allows 10. -_FIXUP_KEYS = ( - ["replace0" + str(i) for i in range(1, 10)] + - ["replace" + str(i) for i in range(10, 51)] -) -# = ['replace01', 'replace02', ..., 'replace50'] - # all the rows that displacements have, in the form # "row0" "???" # "row1" "???" @@ -37,14 +29,35 @@ ) +class IDMan(set): + """Allocate and manage a set of unique IDs.""" + __slots__ = () + + def get_id(self, desired=-1): + """Get a valid ID.""" + + if desired == -1: + # Start with the lowest ID, and look upwards + desired = 1 + + if desired not in self: + # The desired ID is avalible! + self.add(desired) + return desired + + # Check every ID in order to find a valid one + for poss_id in itertools.count(start=1): + if poss_id not in self: + self.add(poss_id) + return poss_id + + def find_empty_id(used_id, desired=-1): """Ensure this item has a unique ID. Used by entities, solids and brush sides to keep their IDs valid. used_id must be sorted, and will be kept sorted. """ - # Add_sorted adds the items while keeping the list sorted, so we never - # have to actually sort the list. if desired == -1: desired = 1 @@ -92,9 +105,9 @@ def __init__( cameras=None, cordons=None, visgroups=None): - self.solid_id = [] # All occupied solid ids - self.face_id = [] # Ditto for faces - self.ent_id = [] # Same for entities + self.solid_id = IDMan() # All occupied solid ids + self.face_id = IDMan() # Ditto for faces + self.ent_id = IDMan() # Same for entities # Allow quick searching for particular groups, without checking # the whole map @@ -349,21 +362,6 @@ def export(self, dest_file=None, inc_version=True): dest_file.close() return string - def get_face_id(self, desired=-1): - """Get an unused face ID. - """ - return find_empty_id(self.face_id, desired) - - def get_brush_id(self, desired=-1): - """Get an unused solid ID. - """ - return find_empty_id(self.solid_id, desired) - - def get_ent_id(self, desired=-1): - """Get an unused entity ID. - """ - return find_empty_id(self.ent_id, desired) - def iter_wbrushes(self, world=True, detail=True): """Iterate through all world and detail solids in the map.""" if world: @@ -543,7 +541,7 @@ def __init__( ): self.map = vmf_file self.sides = sides or [] - self.id = vmf_file.get_brush_id(des_id) + self.id = vmf_file.solid_id.get_id(des_id) self.editor = editor or {} self.hidden = hidden @@ -593,7 +591,7 @@ def parse(vmf_file, tree, hidden=False): if len(editor['visgroup']) == 0: del editor['visgroup'] return Solid( - vmf_file, + vmf_file: VMF, des_id=solid_id, sides=sides, editor=editor, @@ -708,25 +706,30 @@ def __init__( (0, 0, 0), (0, 0, 0) ], - opt=utils.EmptyMapping, des_id=-1, - disp_data={}, + lightmap=16, + smoothing=0, + mat='tools/toolsnodraw', + rotation=0, + uaxis='[0 1 0 0] 0.25', + vaxis='[0 0 -1 0] 0.25', + disp_data=utils.EmptyMapping, ): """ :type planes: list of [(int, int, int)] """ self.map = vmf_file self.planes = [Vec(), Vec(), Vec()] - self.id = vmf_file.get_face_id(des_id) + self.id = vmf_file.face_id.get_id(des_id) for i, pln in enumerate(planes): self.planes[i] = Vec(x=pln[0], y=pln[1], z=pln[2]) - self.lightmap = opt.get("lightmap", 16) - self.smooth = opt.get("smoothing", 0) - self.mat = opt.get("material", "") - self.ham_rot = opt.get("rotation", 0) - self.uaxis = opt.get("uaxis", "[0 1 0 0] 0.25") - self.vaxis = opt.get("vaxis", "[0 1 -1 0] 0.25") - if len(disp_data) > 0: + self.lightmap = lightmap + self.smooth = smoothing + self.mat = mat + self.ham_rot = rotation + self.uaxis = uaxis + self.vaxis = vaxis + if disp_data: self.disp_power = utils.conv_int( disp_data.get('power', '_'), 4) self.disp_pos = Vec.from_str( @@ -765,26 +768,16 @@ def parse(vmf_file, tree): tree['plane', ''] + '"!') - opt = { - 'material': tree['material', ''], - 'uaxis': tree['uaxis', '[0 1 0 0] 0.25'], - 'vaxis': tree['vaxis', '[0 0 -1 0] 0.25'], - 'rotation': utils.conv_int( - tree['rotation', '0']), - 'lightmap': utils.conv_int( - tree['lightmapscale', '16'], 16), - 'smoothing': utils.conv_int( - tree['smoothing_groups', '0']), - } disp_tree = tree.find_key('dispinfo', []) - disp_data = {} if len(disp_tree) > 0: - disp_data['power'] = disp_tree['power', '4'] - disp_data['pos'] = disp_tree['startposition', '4'] - disp_data['flags'] = disp_tree['flags', '0'] - disp_data['elevation'] = disp_tree['elevation', '0'] - disp_data['subdiv'] = disp_tree['subdiv', '0'] - disp_data['allowed_verts'] = {} + disp_data = { + 'power': disp_tree['power', '4'], + 'pos': disp_tree['startposition', '4'], + 'flags': disp_tree['flags', '0'], + 'elevation': disp_tree['elevation', '0'], + 'subdiv': disp_tree['subdiv', '0'], + 'allowed_verts': {}, + } for prop in disp_tree.find_key('allowed_verts', []): disp_data['allowed_verts'][prop.name] = prop.value for v in _DISP_ROWS: @@ -792,26 +785,50 @@ def parse(vmf_file, tree): if len(rows) > 0: rows.sort(key=lambda x: utils.conv_int(x.name[3:])) disp_data[v] = [v.value for v in rows] + else: + disp_data = None + return Side( vmf_file, planes=planes, - opt=opt, des_id=side_id, disp_data=disp_data, + mat=tree['material', ''], + uaxis=tree['uaxis', '[0 1 0 0] 0.25'], + vaxis=tree['vaxis', '[0 0 -1 0] 0.25'], + rotation=utils.conv_int( + tree['rotation', '0']), + lightmap=utils.conv_int( + tree['lightmapscale', '16'], 16), + smoothing=utils.conv_int( + tree['smoothing_groups', '0']), ) def copy(self, des_id=-1): """Duplicate this brush side.""" planes = [p.as_tuple() for p in self.planes] - opt = { - 'material': self.mat, - 'rotation': self.ham_rot, - 'uaxis': self.uaxis, - 'vaxis': self.vaxis, - 'smoothing': self.smooth, - 'lightmap': self.lightmap, - } - return Side(self.map, planes=planes, opt=opt, des_id=des_id) + if self.is_disp: + disp_data = self.disp_data.copy() + disp_data['power'] = self.disp_power + disp_data['flags'] = self.disp_flags + disp_data['elevation'] = self.disp_elev + disp_data['subdiv'] = self.disp_is_subdiv + disp_data['allowed_verts'] = self.disp_allowed_verts + else: + disp_data = {} + + return Side( + self.map, + planes=planes, + des_id=des_id, + mat=self.mat, + rotation=self.ham_rot, + uaxis=self.uaxis, + vaxis=self.vaxis, + smoothing=self.smooth, + lightmap=self.lightmap, + disp_data=disp_data, + ) def export(self, buffer, ind=''): """Generate the strings required to define this side in a VMF.""" @@ -946,7 +963,7 @@ def __init__( self.fixup = EntityFixup(fixup or {}) self.outputs = outputs or [] self.solids = solids or [] - self.id = vmf_file.get_ent_id(ent_id) + self.id = vmf_file.ent_id.get_id(ent_id) self.hidden = hidden self.editor = editor or {'visgroup': []} @@ -999,12 +1016,16 @@ def parse(vmf_file, tree_list, hidden=False): name = item.name if name == "id" and item.value.isnumeric(): ent_id = item.value - elif name in _FIXUP_KEYS: - vals = item.value.split(" ", 1) - var = vals[0][1:] # Strip the $ sign - value = vals[1] + elif name.startswith('replace'): index = item.name[-2:] # Index is the last 2 digits - fixup[var.casefold()] = FixupTuple(var, value, index) + if index.isdigit(): + vals = item.value.split(" ", 1) + var = vals[0][1:] # Strip the $ sign + value = vals[1] + fixup[var.casefold()] = FixupTuple(var, value, index) + else: + # Not a replace value! + keys[name] = item.value elif name == "solid": if item.has_children(): solids.append(Solid.parse(vmf_file, item)) @@ -1048,7 +1069,7 @@ def parse(vmf_file, tree_list, hidden=False): keys[item.name] = item.value return Entity( - vmf_file, + vmf_file: VMF, keys=keys, ent_id=ent_id, solids=solids, From 81b56099aea74ba529f58b3577450219bde7d9bf Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 20 Aug 2015 17:45:12 +1000 Subject: [PATCH 16/30] Fix exporting items Property objects don't take *values anymore. --- src/gameMan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gameMan.py b/src/gameMan.py index 073a6e468..9205d9ce0 100644 --- a/src/gameMan.py +++ b/src/gameMan.py @@ -284,7 +284,7 @@ def export( # Editoritems.txt is composed of a "ItemData" block, holding "Item" and # "Renderables" sections. - editoritems = Property("ItemData", *style.editor.find_all('Item')) + editoritems = Property("ItemData", list(style.editor.find_all('Item'))) for item in sorted(all_items): item_block, editor_parts, config_part = all_items[item].export() From 2b9f83738f84db81ff2bd122b9c77e72c9352c13 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 20 Aug 2015 17:46:33 +1000 Subject: [PATCH 17/30] Add Shorthand for subconditions Conditions can be added directly, without a 'result' surround. This flattens the hierachy a bit in complex conditions. --- src/conditions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/conditions.py b/src/conditions.py index 7c2606b2f..589bc2a0b 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -150,6 +150,11 @@ def parse(cls, prop_block): results.extend(prop.value) # join multiple ones together elif prop.name == 'else': else_results.extend(prop.value) + elif prop.name == 'condition': + # Shortcut to eliminate lots of Result - Condition pairs + results.append(prop) + elif prop.name == 'elsecondition': + else_results.append(prop) elif prop.name == 'priority': try: priority = Decimal(prop.value) From fd3ad59a8e0ce59a566590cc536643e2387f752a Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 20 Aug 2015 17:47:29 +1000 Subject: [PATCH 18/30] Change some things for posIsSolid Use the real scale for pos (it's confusing currently), and properly use the default for RemoveBrush. --- src/conditions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conditions.py b/src/conditions.py index 589bc2a0b..053f6c542 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -799,6 +799,7 @@ def flag_angles(inst, flag): allow_inverse and -inst_normal == normal ) + @make_flag('posIsSolid') def flag_brush_at_loc(inst, flag): """Checks to see if a wall is present at the given location. @@ -818,7 +819,6 @@ def flag_brush_at_loc(inst, flag): sides as well. """ pos = Vec.from_str(flag['pos', '0 0 0']) - pos *= 16 # 16 per quarter-tile pos.z -= 64 # Subtract so origin is the floor-position pos = pos.rotate_by_str(inst['angles', '0 0 0']) @@ -830,7 +830,7 @@ def flag_brush_at_loc(inst, flag): ) result_var = flag['setVar', ''] - should_remove = utils.conv_bool(flag['RemoveBrush'], False) + should_remove = utils.conv_bool(flag['RemoveBrush', False], False) des_type = flag['type', 'any'].casefold() brush = SOLIDS.get(pos.as_tuple(), None) From 400428423c8a3ba11ff77af977a390ac47a0f997 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 20 Aug 2015 18:00:51 +1000 Subject: [PATCH 19/30] Add function to generate prisms --- src/vmfLib.py | 113 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 4 deletions(-) diff --git a/src/vmfLib.py b/src/vmfLib.py index 123d32a0c..0d6a25371 100644 --- a/src/vmfLib.py +++ b/src/vmfLib.py @@ -28,6 +28,12 @@ 'triangle_tags', ) +# Return value for VMF.make_prism() +PrismFace = namedtuple( + "PrismFace", + "solid, top, bottom, north, south, east, west" +) + class IDMan(set): """Allocate and manage a set of unique IDs.""" @@ -427,6 +433,105 @@ def iter_inputs(self, name): if out.target == name: # target yield out + def make_prism(self, p1, p2) -> PrismFace: + """Create an axis-aligned brush connecting the two points. + + A PrismFaces tuple will be returned which containes the six + faces, as well as the solid. + All faces will be textured with tools/toolsnodraw. + """ + b_min = Vec(p1) + b_max = Vec(p1) + b_min.min(p2) + b_max.max(p2) + + f_bottom = Side( + self, + planes=[ # -z side + (b_min.x, b_min.y, b_min.z), + (b_max.x, b_min.y, b_min.z), + (b_max.x, b_max.y, b_min.z), + ], + uaxis='[1 0 0 0] 0.25', + vaxis='[0 -1 0 0] 0.25', + ) + + f_top = Side( + self, + planes=[ # +z side + (b_min.x, b_max.y, b_max.z), + (b_max.x, b_max.y, b_max.z), + (b_max.x, b_min.y, b_max.z), + ], + uaxis='[1 0 0 0] 0.25', + vaxis='[0 -1 0 0] 0.25', + ) + + f_west = Side( + self, + planes=[ # -x side + (b_min.x, b_max.y, b_max.z), + (b_min.x, b_min.y, b_max.z), + (b_min.x, b_min.y, b_min.z), + ], + uaxis='[0 1 0 0] 0.25', + vaxis='[0 0 -1 0] 0.25', + ) + + f_east = Side( + self, + planes=[ # +x side + (b_max.x, b_max.y, b_min.z), + (b_max.x, b_min.y, b_min.z), + (b_max.x, b_min.y, b_max.z), + ], + uaxis='[0 1 0 0] 0.25', + vaxis='[0 0 -1 0] 0.25', + ) + + f_south = Side( + self, + planes=[ # -y side + (b_max.x, b_min.y, b_min.z), + (b_min.x, b_min.y, b_min.z), + (b_min.x, b_min.y, b_max.z), + ], + uaxis='[1 0 0 0] 0.25', + vaxis='[0 0 -1 0] 0.25', + ) + + f_north = Side( + self, + planes=[ # +y side + (b_max.x, b_max.y, b_max.z), + (b_min.x, b_max.y, b_max.z), + (b_min.x, b_max.y, b_min.z), + ], + uaxis='[1 0 0 0] 0.25', + vaxis='[0 0 -1 0] 0.25', + ) + + solid = Solid( + self, + sides=[ + f_bottom, + f_top, + f_north, + f_south, + f_east, + f_west, + ], + ) + return PrismFace( + solid=solid, + top=f_top, + bottom=f_bottom, + north=f_north, + south=f_south, + east=f_east, + west=f_west, + ) + class Camera: def __init__(self, vmf_file, pos, targ): @@ -533,7 +638,7 @@ class Solid: """A single brush, serving as both world brushes and brush entities.""" def __init__( self, - vmf_file, + vmf_file: VMF, des_id=-1, sides=None, editor=None, @@ -591,7 +696,7 @@ def parse(vmf_file, tree, hidden=False): if len(editor['visgroup']) == 0: del editor['visgroup'] return Solid( - vmf_file: VMF, + vmf_file, des_id=solid_id, sides=sides, editor=editor, @@ -950,7 +1055,7 @@ class Entity: """ def __init__( self, - vmf_file, + vmf_file: VMF, keys=None, fixup=None, ent_id=-1, @@ -1069,7 +1174,7 @@ def parse(vmf_file, tree_list, hidden=False): keys[item.name] = item.value return Entity( - vmf_file: VMF, + vmf_file, keys=keys, ent_id=ent_id, solids=solids, From 1a6844002e1b175be6d61dcb4bcb8f86914b4864 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sat, 22 Aug 2015 18:41:23 +1000 Subject: [PATCH 20/30] Upgrade antline logic Allow setting wall and floor textures in CustAntline options --- src/conditions.py | 56 +++++++++++++++++++++++++++++--------------- src/vbsp.py | 59 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 78 insertions(+), 37 deletions(-) diff --git a/src/conditions.py b/src/conditions.py index 053f6c542..a97a19450 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -1091,15 +1091,20 @@ def res_cust_output(inst, res): def res_cust_antline_setup(res): result = { 'instance': res['instance', ''], - 'antline': [p.value for p in res.find_all('straight')], - 'antlinecorner': [p.value for p in res.find_all('corner')], + 'wall_str': [p.value for p in res.find_all('straight')], + 'wall_crn': [p.value for p in res.find_all('corner')], + # If this isn't defined, None signals to use the above textures. + 'floor_str': [p.value for p in res.find_all('straightFloor')] or None, + 'floor_crn': [p.value for p in res.find_all('cornerFloor')] or None, 'outputs': list(res.find_all('addOut')), } if ( - len(result['antline']) == 0 or - len(result['antlinecorner']) == 0 + not result['wall_str'] or + not result['wall_crn'] ): - return None # remove result + # If we don't have two textures, something's wrong. Remove this result. + utils.con_log('custAntline has missing values!') + return None else: return result @@ -1112,38 +1117,51 @@ def res_cust_antline(inst, res): Values: straight: The straight overlay texture. corner: The corner overlay texture. + straightFloor: Alt texture used on straight floor segements (P1 style) + cornerFloor: Alt texture for floor corners (P1 style) + If these aren't set, the wall textures will be used. instance: Use the given indicator_toggle instance instead addOut: A set of additional ouputs to add, pointing at the - toggle instance + toggle instance """ import vbsp + opts = res.value + + # The original textures for straight and corner antlines + straight_ant = vbsp.ANTLINES['straight'] + corner_ant = vbsp.ANTLINES['corner'] + over_name = '@' + inst['targetname'] + '_indicator' for over in ( VMF.by_class['info_overlay'] & VMF.by_target[over_name] ): - random.seed(over['origin']) - new_tex = random.choice( - res.value[ - vbsp.ANTLINES[ - over['material'].casefold() - ] - ] - ) - vbsp.set_antline_mat(over, new_tex, raw_mat=True) + folded_mat = over['material'].casefold() + if folded_mat == straight_ant: + vbsp.set_antline_mat( + over, + opts['wall_str'], + opts['floor_str'], + ) + elif folded_mat == corner_ant: + vbsp.set_antline_mat( + over, + opts['wall_crn'], + opts['floor_crn'], + ) # allow replacing the indicator_toggle instance - if res.value['instance']: + if opts['instance']: for toggle in VMF.by_class['func_instance']: if toggle.fixup['indicator_name', ''] == over_name: - toggle['file'] = res.value['instance'] - if len(res.value['outputs']) > 0: + toggle['file'] = opts['instance'] + if len(opts['outputs']) > 0: for out in inst.outputs[:]: if out.target == toggle['targetname']: # remove the original outputs inst.outputs.remove(out) - for out in res.value['outputs']: + for out in opts['outputs']: # Allow adding extra outputs to customly # trigger the toggle add_output(inst, out, toggle['targetname']) diff --git a/src/vbsp.py b/src/vbsp.py index e6d7cf09b..26c816edf 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -124,9 +124,9 @@ class ORIENT(Enum): ] ANTLINES = { - "signage/indicator_lights/indicator_lights_floor": "antline", - "signage/indicator_lights/indicator_lights_corner_floor": "antlinecorner", - } # these need to be handled separately to accommodate the scale-changing + 'straight' : "signage/indicator_lights/indicator_lights_floor", + 'corner': "signage/indicator_lights/indicator_lights_corner_floor", + } DEFAULTS = { "goo_mist": "0", # Add info_particle_systems to goo pits @@ -1451,10 +1451,15 @@ def get_face_orient(face): return ORIENT.ceiling return ORIENT.wall -def set_antline_mat(over, mat, raw_mat=False): + +def set_antline_mat( + over, + mats, + floor_mats=None, + ): """Set the material on an overlay to the given value, applying options. - If raw_mat is set to 1, use the given texture directly. + floor_mat, if set is an alternate material to use for floors. The material is split into 3 parts, separated by '|': - Scale: the u-axis width of the material, used for clean antlines. - Material: the material @@ -1464,17 +1469,17 @@ def set_antline_mat(over, mat, raw_mat=False): If only 2 parts are given, the overlay is assumed to be dynamic. If one part is given, the scale is assumed to be 0.25 """ - if not raw_mat: - if get_tex('overlay.' + mat + 'floor') != '': - # For P1 style, check to see if the antline is on the floor or - # walls. - direction = Vec(0, 0, 1).rotate_by_str(over['angles']) - if direction == (0, 0, 1) or direction == (0, 0, -1): - mat += 'floor' + if floor_mats: + # For P1 style, check to see if the antline is on the floor or + # walls. + direction = Vec(0, 0, 1).rotate_by_str(over['angles']) + if direction == (0, 0, 1) or direction == (0, 0, -1): + mats = floor_mats - mat = get_tex('overlay.' + mat) + # Choose a random one + random.seed(over['origin']) + mat = random.choice(mats).split('|') - mat = mat.split('|') if len(mat) == 2: # rescale antlines if needed over['endu'], over['material'] = mat @@ -1495,6 +1500,12 @@ def change_overlays(): sign_inst = get_opt('signInst') if sign_inst == "NONE": sign_inst = None + + ant_str = settings['textures']['overlay.antline'], + ant_str_floor = settings['textures']['overlay.antlinefloor'], + ant_corn = settings['textures']['overlay.antlinecorner'], + ant_corn_floor = settings['textures']['overlay.antlinecornerfloor'], + for over in VMF.by_class['info_overlay']: if (over['targetname'] == 'exitdoor_stickman' or over['targetname'] == 'exitdoor_arrow'): @@ -1508,8 +1519,10 @@ def change_overlays(): # useless info_overlay_accessors for these signs. del over['targetname'] - if over['material'].casefold() in TEX_VALVE: - sign_type = TEX_VALVE[over['material'].casefold()] + case_mat = over['material'].casefold() + + if case_mat in TEX_VALVE: + sign_type = TEX_VALVE[case_mat] if sign_inst is not None: new_inst = VMF.create_ent( classname='func_instance', @@ -1520,8 +1533,18 @@ def change_overlays(): new_inst.fixup['mat'] = sign_type.replace('overlay.', '') over['material'] = get_tex(sign_type) - if over['material'].casefold() in ANTLINES: - set_antline_mat(over, ANTLINES[over['material'].casefold()]) + if case_mat == ANTLINES['straight']: + set_antline_mat( + over, + ant_str, + ant_str_floor, + ) + elif case_mat == ANTLINES['corner']: + set_antline_mat( + over, + ant_corn, + ant_corn_floor, + ) def change_trig(): From 27466e55aa8b69aecca0b77d1d2e88df3b8dbf19 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sat, 22 Aug 2015 18:42:32 +1000 Subject: [PATCH 21/30] Add seperate floor/wall special.white/black This allows using both checkered and portal-here textures, as well as solving the incorrect rotation of the latter on floors. Add a condition result which applies this to additional brushes. --- src/conditions.py | 24 +++++++++++++++++++++ src/vbsp.py | 53 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/src/conditions.py b/src/conditions.py index a97a19450..993ee38ab 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -2005,3 +2005,27 @@ def track_scan( # If the next piece is an end section, add it then quit tr_set.add(track) return + + +@make_result('MarkFaceSpecial') +def res_set_special_side(inst, res): + """Set the brush face at a location to a particular texture. + + """ + pos = Vec.from_str(res['pos', '0 0 0']) + pos.z -= 64 # Subtract so origin is the floor-position + pos = pos.rotate_by_str(inst['angles', '0 0 0']) + + # Relative to the instance origin + pos += Vec.from_str(inst['origin', '0 0 0']) + + norm = Vec.from_str(res['dir', '0 0 -1']).rotate_by_str( + inst['angles', '0 0 0'] + ) + + brush = SOLIDS.get(pos.as_tuple(), None) + ':type brush: solidGroup' + + if brush and brush.normal == norm: + import vbsp + vbsp.set_special_mat(brush.face, str(brush.color)) \ No newline at end of file diff --git a/src/vbsp.py b/src/vbsp.py index 26c816edf..8ce3e9950 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -78,6 +78,8 @@ # These replacements are deactivated when unset ('', 'special.white'), ('', 'special.black'), + ('', 'special.white_wall'), + ('', 'special.black_wall'), ('', 'special.white_gap'), ('', 'special.black_gap'), ('', 'special.goo_wall'), @@ -88,6 +90,7 @@ 'overlay.antline'), ('1|signage/indicator_lights/indicator_lights_corner_floor', 'overlay.antlinecorner'), + # This is for the P1 style, where antlines use different textures # on the floor and wall. # We just use the regular version if unset. @@ -102,6 +105,14 @@ class ORIENT(Enum): ceiling = 3 ceil = 3 + def __str__(self): + if self is ORIENT.floor: + return 'floor' + elif self is ORIENT.wall: + return 'wall' + elif self is ORIENT.ceiling: + return 'ceiling' + WHITE_PAN = [ "tile/white_floor_tile002a", "tile/white_wall_tile003a", @@ -1660,18 +1671,15 @@ def change_func_brush(): if (side.mat.casefold() == "anim_wp/framework/squarebeams" and "special.edge" in settings['textures']): side.mat = get_tex("special.edge") - elif side.mat.casefold() in WHITE_PAN: + continue + + if side.mat.casefold() in WHITE_PAN: brush_type = "white" - if not get_tex("special.white") == "": - side.mat = get_tex("special.white") - elif not alter_mat(side): - side.mat = get_tex("white.wall") + set_special_mat(side, 'white') + elif side.mat.casefold() in BLACK_PAN: brush_type = "black" - if not get_tex("special.black") == "": - side.mat = get_tex("special.black") - elif not alter_mat(side): - side.mat = get_tex("black.wall") + set_special_mat(side, 'black') else: if side.mat.casefold() == 'metal/metalgrate018': is_grating = True @@ -1683,6 +1691,7 @@ def change_func_brush(): side.vaxis = " ".join(split_v) alter_mat(side) # for gratings, laserfields and some others + # The style blanked the material, so delete the brush if side.mat == '': delete_brush = True @@ -1703,20 +1712,44 @@ def change_func_brush(): if "-model_arms" in parent: # is this an angled panel?: # strip only the model_arms off the end targ = '-'.join(parent.split("-")[:-1]) + # Now find the associated instance for ins in ( VMF.by_class['func_instance'] & VMF.by_target[targ] ): if make_static_pan(ins, brush_type): - # delete the brush, we don't want it if we made a static one + # delete the brush, we don't want it if we made a + # static one VMF.remove_ent(brush) else: + # Oherwise, rename the brush to -brush, so the panel + # can send inputs itself. (This allows removing 1 + # logic_auto.) brush['targetname'] = brush['targetname'].replace( '_panel_top', '-brush', ) +def set_special_mat(face, side_type): + """Set a face to a special texture. + + Those include checkers or portal-here tiles, used on flip + and angled panels. + side_type should be either 'white' or 'black'. + """ + # We use a wall-specific texture, or the floor texture, + # or fallback to regular textures + rep_texture = 'special.' + side_type + orient = get_face_orient(face) + if orient is ORIENT.wall and get_tex(rep_texture + '_wall'): + face.mat = get_tex(rep_texture + '_wall') + elif get_tex(rep_texture): + face.mat = get_tex(rep_texture) + elif not alter_mat(face): + face.mat = get_tex(side_type + '.' + str(orient)) + + def make_static_pan(ent, pan_type): """Convert a regular panel into a static version. From 2ac9d8a3f1a4c222e72a9ecc36b5c36b32ff4732 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Tue, 25 Aug 2015 15:57:48 +1000 Subject: [PATCH 22/30] Fix antline texturing These shouldn't be tuples! --- src/vbsp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vbsp.py b/src/vbsp.py index 8ce3e9950..02bad1b2d 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -1512,10 +1512,10 @@ def change_overlays(): if sign_inst == "NONE": sign_inst = None - ant_str = settings['textures']['overlay.antline'], - ant_str_floor = settings['textures']['overlay.antlinefloor'], - ant_corn = settings['textures']['overlay.antlinecorner'], - ant_corn_floor = settings['textures']['overlay.antlinecornerfloor'], + ant_str = settings['textures']['overlay.antline'] + ant_str_floor = settings['textures']['overlay.antlinefloor'] + ant_corn = settings['textures']['overlay.antlinecorner'] + ant_corn_floor = settings['textures']['overlay.antlinecornerfloor'] for over in VMF.by_class['info_overlay']: if (over['targetname'] == 'exitdoor_stickman' or From a815c081f679d1533b5ced19a940062d3f151a4c Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Tue, 25 Aug 2015 15:58:21 +1000 Subject: [PATCH 23/30] Add more configuration for squarebeams textures --- src/vbsp.py | 80 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/src/vbsp.py b/src/vbsp.py index 02bad1b2d..677bd3a41 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -83,6 +83,7 @@ ('', 'special.white_gap'), ('', 'special.black_gap'), ('', 'special.goo_wall'), + ('', 'special.edge_special'), # And these defaults have the extra scale information, which isn't # in the maps. @@ -148,6 +149,13 @@ def __str__(self): "random_blackwall_scale": "0", # P1 style randomly sized black walls + "rotate_edge": "0", # Rotate squarebeams textures 90 degrees. + "edge_off": "53.3335", # The offset used on squarebeams. + "edge_scale": "0.15", # The scale on squarebeams textures + "rotate_edge_special": "0", # Ditto for angled/flip panels + "edge_off_special": "53.3335", + "edge_scale_special": "0.15", + # Reset offsets for all white/black brushes, so embedface has correct # texture matching "tile_texture_lock": "1", @@ -230,7 +238,7 @@ def __str__(self): ################## -def get_opt(name): +def get_opt(name) -> str: return settings['options'][name.casefold()] @@ -973,6 +981,7 @@ def add_goo_mist(sides): particle='water_mist_256', ) + def fit_goo_mist( sides, needs_mist, @@ -1009,6 +1018,7 @@ def fit_goo_mist( for (x, y) in iter_grid(grid_x, grid_y, 128): needs_mist.remove((pos.x+x, pos.y+y, pos.z)) + def change_goo_sides(): """Replace the textures on the sides of goo with specific ones. @@ -1053,6 +1063,7 @@ def change_goo_sides(): face.mat = get_tex('special.goo_wall') utils.con_log("Done!") + def collapse_goo_trig(): """Collapse the goo triggers to only use 2 entities for all pits.""" utils.con_log('Collapsing goo triggers...') @@ -1105,6 +1116,30 @@ def remove_static_ind_toggles(): utils.con_log('Done!') +def fix_squarebeams(face, rotate, offset: str, scale: str): + '''Fix a squarebeams brush for use in other styles. + + If rotate is True, rotate the texture 90 degrees. + offset is the offset for the texture. + ''' + uaxis = face.uaxis.split(' ') + vaxis = face.vaxis.split(' ') + if rotate: + # To rotate, swap the two values + uaxis, vaxis = vaxis, uaxis + + # We want to modify the value with an offset + if uaxis[3] != '0]': + uaxis[3] = offset + ']' + uaxis[4] = scale + else: + vaxis[3] = offset + ']' + vaxis[4] = scale + + face.uaxis = ' '.join(uaxis) + face.vaxis = ' '.join(vaxis) + + def change_brush(): """Alter all world/detail brush textures to use the configured ones.""" utils.con_log("Editing Brushes...") @@ -1265,6 +1300,7 @@ def face_seed(face): origin[axis] = (origin[axis] // 128) * 128 + 64 return origin.join(' ') + def reset_tex_offset(face): """Force all white/black walls to 0 offsets""" uaxis = face.uaxis.split() @@ -1274,6 +1310,7 @@ def reset_tex_offset(face): face.uaxis = ' '.join(uaxis) face.vaxis = ' '.join(vaxis) + def get_grid_sizes(face: VLib.Side): """Determine the grid sizes that fits on this brush.""" bbox_min, bbox_max = face.get_bbox() @@ -1295,12 +1332,20 @@ def get_grid_sizes(face: VLib.Side): if u % 32 == 0 and v % 32 == 0: # 4x4 grid return "0.25", + def random_walls(): """The original wall style, with completely randomised walls.""" scale_walls = get_bool_opt("random_blackwall_scale") + rotate_edge = get_bool_opt('rotate_edge') texture_lock = get_bool_opt('tile_texture_lock', True) + edge_off = get_opt('edge_off') + edge_scale = get_opt('edge_scale') + for solid in VMF.iter_wbrushes(world=True, detail=True): for face in solid: + if face.mat.casefold() == 'anim_wp/framework/squarebeams': + fix_squarebeams(face, rotate_edge, edge_off, edge_scale) + orient = get_face_orient(face) # Only modify black walls and ceilings if (scale_walls and @@ -1342,6 +1387,9 @@ def clump_walls(): others = {} texture_lock = get_bool_opt('tile_texture_lock', True) + rotate_edge = get_bool_opt('rotate_edge') + edge_off = get_opt('edge_off') + edge_scale = get_opt('edge_scale') for solid in VMF.iter_wbrushes(world=True, detail=True): # first build a dict of all textures and their locations... @@ -1358,6 +1406,8 @@ def clump_walls(): # use random textures. Don't add them here. They also aren't # on grid. alter_mat(face) + if mat == 'anim_wp/framework/squarebeams': + fix_squarebeams(face, edge_off, rotate_edge, edge_scale) continue if face.mat in GOO_TEX: @@ -1465,8 +1515,8 @@ def get_face_orient(face): def set_antline_mat( over, - mats, - floor_mats=None, + mats: list, + floor_mats: list=None, ): """Set the material on an overlay to the given value, applying options. @@ -1489,6 +1539,7 @@ def set_antline_mat( # Choose a random one random.seed(over['origin']) + utils.con_log(mats) mat = random.choice(mats).split('|') if len(mat) == 2: @@ -1646,6 +1697,18 @@ def change_func_brush(): grating_inst = get_opt("grating_inst") grating_scale = get_opt("grating_scale") + if get_tex('special.edge_special') == '': + edge_tex = 'special.edge' + rotate_edge = get_bool_opt('rotate_edge', False) + edge_off = get_opt('edge_off') + edge_scale = get_opt('edge_scale') + else: + edge_tex = 'special.edge_special' + rotate_edge = get_bool_opt('rotate_edge_special', False) + edge_off = get_opt('edge_off_special') + edge_scale = get_opt('edge_scale_special') + utils.con_log('Special tex:', rotate_edge, edge_off, edge_scale) + if grating_inst == "NONE": grating_inst = None for brush in ( @@ -1668,9 +1731,14 @@ def change_func_brush(): is_grating = False delete_brush = False for side in brush.sides(): - if (side.mat.casefold() == "anim_wp/framework/squarebeams" and - "special.edge" in settings['textures']): - side.mat = get_tex("special.edge") + if side.mat.casefold() == "anim_wp/framework/squarebeams": + side.mat = get_tex(edge_tex) + fix_squarebeams( + side, + rotate_edge, + edge_off, + edge_scale, + ) continue if side.mat.casefold() in WHITE_PAN: From 0f7c39a6e2fcd316365320964627a182ac53ee44 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Tue, 25 Aug 2015 18:09:49 +1000 Subject: [PATCH 24/30] Replace MarkFaceSpecial with AlterTexture This allows switching the texture used on a face to a variety of different options. --- src/conditions.py | 95 ++++++++++++++++++++++++++++++++++++++++++++--- src/vbsp.py | 27 +++++++++++--- 2 files changed, 112 insertions(+), 10 deletions(-) diff --git a/src/conditions.py b/src/conditions.py index 993ee38ab..ec7f7b1e7 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -1151,6 +1151,9 @@ def res_cust_antline(inst, res): opts['floor_crn'], ) + # Ensure this isn't overriden later! + vbsp.IGNORED_OVERLAYS.add(over) + # allow replacing the indicator_toggle instance if opts['instance']: for toggle in VMF.by_class['func_instance']: @@ -2007,11 +2010,33 @@ def track_scan( return -@make_result('MarkFaceSpecial') -def res_set_special_side(inst, res): +@make_result('AlterTexture', 'AlterTex', 'AlterFace') +def res_set_texture(inst, res): """Set the brush face at a location to a particular texture. + pos is the position, relative to the instance + (0 0 0 is the floor-surface). + dir is the normal of the texture. + If gridPos is true, the position will be snapped so it aligns with + the 128 brushes (Useful with fizzler/light strip items). + + tex is the texture used. + If tex begins and ends with '<>', certain + textures will be used based on style: + - If tex is '', the brush will be given a special texture + like angled and clear panels. + - '' and '' will use the regular textures for the + given color. + - '', '', '', ' will use + the given wall-sizes. If on floors or ceilings these always use 4x4. + - '<2x2>' or '<4x4>' will force to the given wall-size, keeping color. + - '' and '' will use a special texture + of the given color. + If tex begins and ends with '[]', it is an option in the 'Textures' list. + These are composed of a group and texture, separated by '.'. 'white.wall' + are the white wall textures; 'special.goo' is the goo texture. """ + import vbsp pos = Vec.from_str(res['pos', '0 0 0']) pos.z -= 64 # Subtract so origin is the floor-position pos = pos.rotate_by_str(inst['angles', '0 0 0']) @@ -2023,9 +2048,69 @@ def res_set_special_side(inst, res): inst['angles', '0 0 0'] ) + if utils.conv_bool(res['gridpos', '0']): + for axis in 'xyz': + # Don't realign things in the normal's axis - + # those are already fine. + if not norm[axis]: + pos[axis] //= 128 + pos[axis] *= 128 + pos[axis] += 64 + brush = SOLIDS.get(pos.as_tuple(), None) ':type brush: solidGroup' - if brush and brush.normal == norm: - import vbsp - vbsp.set_special_mat(brush.face, str(brush.color)) \ No newline at end of file + if not brush or brush.normal != norm: + return + + tex = res['tex'] + + if tex.startswith('[') and tex.endswith(']'): + brush.face.mat = vbsp.get_tex(tex[1:-1]) + brush.face.mat = tex + elif tex.startswith('<') and tex.endswith('>'): + # Special texture names! + tex = tex[1:-1].casefold() + if tex == 'white': + brush.face.mat = 'tile/white_wall_tile003a' + elif tex == 'black': + brush.face.mat = 'metal/black_wall_metal_002c' + + if tex == 'black' or tex == 'white': + # For these two, run the regular logic to apply textures + # correctly. + vbsp.alter_mat( + brush.face, + vbsp.face_seed(brush.face), + vbsp.get_bool_opt('tile_texture_lock', True), + ) + + if tex == 'special': + vbsp.set_special_mat(brush.face, str(brush.color)) + elif tex == 'special-white': + vbsp.set_special_mat(brush.face, 'white') + return + elif tex == 'special-black': + vbsp.set_special_mat(brush.face, 'black') + + # Do <4x4>, , etc + color = str(brush.color) + if tex.startswith('black') or tex.endswith('white'): + # Override the color used for 2x2/4x4 brushes + color = tex[:5] + if tex.endswith('2x2') or tex.endswith('4x4'): + # 4x4 and 2x2 instructions are ignored on floors and ceilings. + orient = vbsp.get_face_orient(brush.face) + if orient == vbsp.ORIENT.wall: + brush.face.mat = vbsp.get_tex( + color + '.' + tex[-3:] + ) + else: + brush.face.mat = vbsp.get_tex( + color + '.' + str(orient) + ) + else: + brush.face.mat = tex + + # Don't allow this to get overwritten later. + vbsp.IGNORED_FACES.add(brush.face) diff --git a/src/vbsp.py b/src/vbsp.py index 677bd3a41..7cfc3fc1c 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -114,20 +114,23 @@ def __str__(self): elif self is ORIENT.ceiling: return 'ceiling' +# The textures used for white surfaces. WHITE_PAN = [ "tile/white_floor_tile002a", "tile/white_wall_tile003a", "tile/white_wall_tile003h", - "tile/white_wall_tile003c", - "tile/white_wall_tile003f", + + "tile/white_wall_tile003c", # 2x2 + "tile/white_wall_tile003f", # 4x4 ] +# Ditto for black surfaces. BLACK_PAN = [ "metal/black_floor_metal_001c", "metal/black_wall_metal_002c", "metal/black_wall_metal_002e", - "metal/black_wall_metal_002a", - "metal/black_wall_metal_002b", + "metal/black_wall_metal_002a", # 2x2 + "metal/black_wall_metal_002b", # 4x4 ] GOO_TEX = [ @@ -233,6 +236,11 @@ def __str__(self): GAME_MODE = 'ERR' IS_PREVIEW = 'ERR' +# These are faces & overlays which have been forceably set by conditions, +# and will not be overwritten later. +IGNORED_FACES = set() +IGNORED_OVERLAYS = set() + ################## # UTIL functions # ################## @@ -1326,7 +1334,7 @@ def get_grid_sizes(face: VLib.Side): raise Exception(str(dim) + ' not on grid!') if u % 128 == 0 and v % 128 == 0: # regular square - return "0.25", "0.5", "1" + return "0.25", "0.5", "0.5", "1", "1", if u % 64 == 0 and v % 64 == 0: # 2x2 grid return "0.5", if u % 32 == 0 and v % 32 == 0: # 4x4 grid @@ -1343,6 +1351,9 @@ def random_walls(): for solid in VMF.iter_wbrushes(world=True, detail=True): for face in solid: + if face in IGNORED_FACES: + continue + if face.mat.casefold() == 'anim_wp/framework/squarebeams': fix_squarebeams(face, rotate_edge, edge_off, edge_scale) @@ -1394,6 +1405,9 @@ def clump_walls(): for solid in VMF.iter_wbrushes(world=True, detail=True): # first build a dict of all textures and their locations... for face in solid: + if face in IGNORED_FACES: + continue + mat = face.mat.casefold() if mat in ( 'glass/glasswindow007a_less_shiny', @@ -1569,6 +1583,9 @@ def change_overlays(): ant_corn_floor = settings['textures']['overlay.antlinecornerfloor'] for over in VMF.by_class['info_overlay']: + if over in IGNORED_OVERLAYS: + continue + if (over['targetname'] == 'exitdoor_stickman' or over['targetname'] == 'exitdoor_arrow'): if get_bool_opt("remove_exit_signs"): From fe4f856e9b893c11922677f2443f921e556cebb4 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 27 Aug 2015 16:10:36 +1000 Subject: [PATCH 25/30] Fix squarebeams options with clumping algorithm --- src/vbsp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vbsp.py b/src/vbsp.py index 7cfc3fc1c..b904fbdbc 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -1421,7 +1421,7 @@ def clump_walls(): # on grid. alter_mat(face) if mat == 'anim_wp/framework/squarebeams': - fix_squarebeams(face, edge_off, rotate_edge, edge_scale) + fix_squarebeams(face, rotate_edge, edge_off, edge_scale) continue if face.mat in GOO_TEX: From df3115b7b002d4b8578ff03994b5170d50c2b853 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 27 Aug 2015 16:38:08 +1000 Subject: [PATCH 26/30] Fix this docstring --- src/vbsp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vbsp.py b/src/vbsp.py index b904fbdbc..990d4b518 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -1927,7 +1927,7 @@ def fix_inst(): def fix_worldspawn(): - """Adjust some properties on WorldSpawn.""""" + """Adjust some properties on WorldSpawn.""" utils.con_log("Editing WorldSpawn") if VMF.spawn['paintinmap'] != '1': # if PeTI thinks there should be paint, don't touch it From a90a83b19141fbfc9116347340a37875ff17775e Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sat, 29 Aug 2015 12:36:49 +1000 Subject: [PATCH 27/30] Parse u and v axes on faces This cuts out a lot of uaxis.split() ... ' '.join(uaxis) boilerplate in vbsp and conditions. --- src/conditions.py | 23 ++++------ src/vbsp.py | 95 +++++++++++++-------------------------- src/vmfLib.py | 112 +++++++++++++++++++++++++++++++++++----------- 3 files changed, 125 insertions(+), 105 deletions(-) diff --git a/src/conditions.py b/src/conditions.py index ec7f7b1e7..d1491425a 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -1322,11 +1322,8 @@ def res_cust_fizzler(base_inst, res): if side.mat.casefold() == 'effects/fizzler': side.mat = laser_tex - uaxis = side.uaxis.split(" ") - vaxis = side.vaxis.split(" ") - # the format is like "[1 0 0 -393.4] 0.25" - side.uaxis = ' '.join(uaxis[:3]) + ' 0] 0.25' - side.vaxis = ' '.join(vaxis[:4]) + ' 0.25' + side.uaxis.offset = 0 + side.scale = 0.25 else: side.mat = nodraw_tex else: @@ -1408,23 +1405,19 @@ def convert_to_laserfield( side.mat = laser_tex # Now we figure out the corrrect u/vaxis values for the texture. - uaxis = side.uaxis.split(" ") - vaxis = side.vaxis.split(" ") - # the format is like "[1 0 0 -393.4] 0.25" size = 0 offset = 0 for i, wid in enumerate(dimensions): if wid > size: size = int(wid) offset = int(bounds_min[i]) - side.uaxis = ( - " ".join(uaxis[:3]) + " " + - # texture offset to fit properly - str(tex_width/size * -offset) + "] " + - str(size/tex_width) # scaling - ) + # texture offset to fit properly + side.uaxis.offset= tex_width/size * -offset + side.uaxis.scale= size/tex_width # scaling + # heightwise it's always the same - side.vaxis = (" ".join(vaxis[:3]) + " 256] 0.25") + side.vaxis.offset = 256 + side.vaxis.scale = 0.25 @make_result('condition') diff --git a/src/vbsp.py b/src/vbsp.py index 990d4b518..8eb4d19d0 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -153,10 +153,10 @@ def __str__(self): "random_blackwall_scale": "0", # P1 style randomly sized black walls "rotate_edge": "0", # Rotate squarebeams textures 90 degrees. - "edge_off": "53.3335", # The offset used on squarebeams. + "reset_edge_off": "0", # Reset the scale on "edge_scale": "0.15", # The scale on squarebeams textures "rotate_edge_special": "0", # Ditto for angled/flip panels - "edge_off_special": "53.3335", + "reset_edge_off_special": "", "edge_scale_special": "0.15", # Reset offsets for all white/black brushes, so embedface has correct @@ -302,7 +302,7 @@ def alter_mat(face, seed=None, texture_lock=True): face.mat = get_tex(surf_type + '.' + orient) if not texture_lock: - reset_tex_offset(face) + face.offset = 0 return True elif mat in TEX_FIZZLER: @@ -1124,36 +1124,36 @@ def remove_static_ind_toggles(): utils.con_log('Done!') -def fix_squarebeams(face, rotate, offset: str, scale: str): +def fix_squarebeams(face, rotate, reset_offset: bool, scale: float): '''Fix a squarebeams brush for use in other styles. If rotate is True, rotate the texture 90 degrees. offset is the offset for the texture. ''' - uaxis = face.uaxis.split(' ') - vaxis = face.vaxis.split(' ') if rotate: # To rotate, swap the two values - uaxis, vaxis = vaxis, uaxis + face.uaxis, face.vaxis = face.vaxis, face.uaxis # We want to modify the value with an offset - if uaxis[3] != '0]': - uaxis[3] = offset + ']' - uaxis[4] = scale + if face.uaxis.offset != 0: + targ = face.uaxis else: - vaxis[3] = offset + ']' - vaxis[4] = scale + targ = face.vaxis - face.uaxis = ' '.join(uaxis) - face.vaxis = ' '.join(vaxis) + if reset_offset: + targ.offset = 0 + targ.scale = scale + + utils.con_log(face.uaxis, face.vaxis) def change_brush(): """Alter all world/detail brush textures to use the configured ones.""" utils.con_log("Editing Brushes...") glass_inst = get_opt('glass_inst') - glass_scale = get_opt('glass_scale') - goo_scale = get_opt('goo_scale') + glass_scale = utils.conv_float(get_opt('glass_scale'), 0.15) + goo_scale = utils.conv_float(get_opt('goo_scale'), 1) + # Goo mist must be enabled by both the style and the user. make_goo_mist = get_bool_opt('goo_mist') and utils.conv_bool( settings['style_vars'].get('AllowGooMist', '1') @@ -1213,20 +1213,11 @@ def change_brush(): mist_solids.add( solid.get_origin().as_tuple() ) - - split_u = face.uaxis.split() - split_v = face.vaxis.split() - split_u[-1] = goo_scale # Apply goo scaling - split_v[-1] = goo_scale - face.uaxis = " ".join(split_u) - face.vaxis = " ".join(split_v) + # Apply goo scaling + face.scale = goo_scale if face.mat.casefold() == "glass/glasswindow007a_less_shiny": - split_u = face.uaxis.split() - split_v = face.vaxis.split() - split_u[-1] = glass_scale # apply the glass scaling option - split_v[-1] = glass_scale - face.uaxis = " ".join(split_u) - face.vaxis = " ".join(split_v) + # Apply the glass scaling option + face.scale = glass_scale settings['has_attr']['glass'] = True is_glass = True if is_glass and glass_inst is not None: @@ -1309,16 +1300,6 @@ def face_seed(face): return origin.join(' ') -def reset_tex_offset(face): - """Force all white/black walls to 0 offsets""" - uaxis = face.uaxis.split() - vaxis = face.vaxis.split() - uaxis[3] = '0]' - vaxis[3] = '0]' - face.uaxis = ' '.join(uaxis) - face.vaxis = ' '.join(vaxis) - - def get_grid_sizes(face: VLib.Side): """Determine the grid sizes that fits on this brush.""" bbox_min, bbox_max = face.get_bbox() @@ -1346,8 +1327,8 @@ def random_walls(): scale_walls = get_bool_opt("random_blackwall_scale") rotate_edge = get_bool_opt('rotate_edge') texture_lock = get_bool_opt('tile_texture_lock', True) - edge_off = get_opt('edge_off') - edge_scale = get_opt('edge_scale') + edge_off = get_bool_opt('reset_edge_off', False) + edge_scale = utils.conv_float(get_opt('edge_scale'), 0.15) for solid in VMF.iter_wbrushes(world=True, detail=True): for face in solid: @@ -1367,13 +1348,7 @@ def random_walls(): # randomly scale textures to achieve the P1 multi-sized # black tile look without custom textues scale = random.choice(get_grid_sizes(face)) - split = face.uaxis.split() - split[-1] = scale - face.uaxis = " ".join(split) - - split = face.vaxis.split() - split[-1] = scale - face.vaxis = " ".join(split) + face.scale = scale alter_mat(face, face_seed(face), texture_lock) @@ -1399,8 +1374,8 @@ def clump_walls(): texture_lock = get_bool_opt('tile_texture_lock', True) rotate_edge = get_bool_opt('rotate_edge') - edge_off = get_opt('edge_off') - edge_scale = get_opt('edge_scale') + edge_off = get_bool_opt('reset_edge_off', False) + edge_scale = utils.conv_float(get_opt('edge_scale'), 0.15) for solid in VMF.iter_wbrushes(world=True, detail=True): # first build a dict of all textures and their locations... @@ -1494,7 +1469,7 @@ def clump_walls(): if pos_min <= Vec(pos) <= pos_max and side.mat == wall_type: side.mat = tex if not texture_lock: - reset_tex_offset(side) + side.offset = 0 # Return to the map_seed state. random.setstate(state) @@ -1712,18 +1687,18 @@ def change_func_brush(): """Edit func_brushes.""" utils.con_log("Editing Brush Entities...") grating_inst = get_opt("grating_inst") - grating_scale = get_opt("grating_scale") + grating_scale = utils.conv_float(get_opt("grating_scale"), 0.15) if get_tex('special.edge_special') == '': edge_tex = 'special.edge' rotate_edge = get_bool_opt('rotate_edge', False) - edge_off = get_opt('edge_off') - edge_scale = get_opt('edge_scale') + edge_off = get_bool_opt('reset_edge_off') + edge_scale = utils.conv_float(get_opt('edge_scale'), 0.15) else: edge_tex = 'special.edge_special' rotate_edge = get_bool_opt('rotate_edge_special', False) - edge_off = get_opt('edge_off_special') - edge_scale = get_opt('edge_scale_special') + edge_off = get_bool_opt('reset_edge_off_special') + edge_scale = utils.conv_float(get_opt('edge_scale_special'), 0.15) utils.con_log('Special tex:', rotate_edge, edge_off, edge_scale) if grating_inst == "NONE": @@ -1768,15 +1743,9 @@ def change_func_brush(): else: if side.mat.casefold() == 'metal/metalgrate018': is_grating = True - split_u = side.uaxis.split() - split_v = side.vaxis.split() - split_u[-1] = grating_scale # apply the grtating - split_v[-1] = grating_scale # scaling option - side.uaxis = " ".join(split_u) - side.vaxis = " ".join(split_v) + side.scale = grating_scale alter_mat(side) # for gratings, laserfields and some others - # The style blanked the material, so delete the brush if side.mat == '': delete_brush = True diff --git a/src/vmfLib.py b/src/vmfLib.py index 0d6a25371..ff71b5f31 100644 --- a/src/vmfLib.py +++ b/src/vmfLib.py @@ -452,8 +452,8 @@ def make_prism(self, p1, p2) -> PrismFace: (b_max.x, b_min.y, b_min.z), (b_max.x, b_max.y, b_min.z), ], - uaxis='[1 0 0 0] 0.25', - vaxis='[0 -1 0 0] 0.25', + uaxis=UVAxis(1, 0, 0), + vaxis=UVAxis(0, -1, 0), ) f_top = Side( @@ -463,8 +463,8 @@ def make_prism(self, p1, p2) -> PrismFace: (b_max.x, b_max.y, b_max.z), (b_max.x, b_min.y, b_max.z), ], - uaxis='[1 0 0 0] 0.25', - vaxis='[0 -1 0 0] 0.25', + uaxis=UVAxis(1, 0, 0), + vaxis=UVAxis(0, -1, 0), ) f_west = Side( @@ -474,8 +474,8 @@ def make_prism(self, p1, p2) -> PrismFace: (b_min.x, b_min.y, b_max.z), (b_min.x, b_min.y, b_min.z), ], - uaxis='[0 1 0 0] 0.25', - vaxis='[0 0 -1 0] 0.25', + uaxis=UVAxis(0, 1, 0), + vaxis=UVAxis(0, 0, -1), ) f_east = Side( @@ -485,8 +485,8 @@ def make_prism(self, p1, p2) -> PrismFace: (b_max.x, b_min.y, b_min.z), (b_max.x, b_min.y, b_max.z), ], - uaxis='[0 1 0 0] 0.25', - vaxis='[0 0 -1 0] 0.25', + uaxis=UVAxis(0, 1, 0), + vaxis=UVAxis(0, 0, -1), ) f_south = Side( @@ -496,8 +496,8 @@ def make_prism(self, p1, p2) -> PrismFace: (b_min.x, b_min.y, b_min.z), (b_min.x, b_min.y, b_max.z), ], - uaxis='[1 0 0 0] 0.25', - vaxis='[0 0 -1 0] 0.25', + uaxis=UVAxis(1, 0, 0), + vaxis=UVAxis(0, 0, -1), ) f_north = Side( @@ -507,8 +507,8 @@ def make_prism(self, p1, p2) -> PrismFace: (b_min.x, b_max.y, b_max.z), (b_min.x, b_max.y, b_min.z), ], - uaxis='[1 0 0 0] 0.25', - vaxis='[0 0 -1 0] 0.25', + uaxis=UVAxis(1, 0, 0), + vaxis=UVAxis(0, 0, -1), ) solid = Solid( @@ -781,6 +781,54 @@ def translate(self, diff): s.translate(diff) +class UVAxis: + """Values saved into Side.uaxis and Side.vaxis. + + These define the alignment of textures on a face. + """ + __slots__ = [ + 'x', 'y', 'z', + 'scale', + 'offset', + ] + + def __init__(self, x, y, z, offset=0.0, scale=0.25): + self.x = x + self.y = y + self.z = z + self.offset = offset + self.scale = scale + + @staticmethod + def parse(value): + vals = value.split() + return UVAxis( + x=float(vals[0].lstrip('[')), + y=float(vals[1]), + z=float(vals[2]), + offset=float(vals[3].rstrip(']')), + scale=float(vals[4]), + ) + + def copy(self): + return UVAxis( + x=self.x, + y=self.y, + z=self.z, + offset=self.offset, + scale=self.scale, + ) + + def __str__(self): + return '[{x} {y} {z} {off}] {scale}'.format( + x=self.x, + y=self.y, + z=self.z, + off=self.offset, + scale=self.scale, + ) + + class Side: """A brush face.""" __slots__ = [ @@ -806,19 +854,19 @@ class Side: def __init__( self, vmf_file, - planes=[ + planes=( (0, 0, 0), (0, 0, 0), (0, 0, 0) - ], + ), des_id=-1, lightmap=16, smoothing=0, mat='tools/toolsnodraw', rotation=0, - uaxis='[0 1 0 0] 0.25', - vaxis='[0 0 -1 0] 0.25', - disp_data=utils.EmptyMapping, + uaxis=None, + vaxis=None, + disp_data: dict=None, ): """ :type planes: list of [(int, int, int)] @@ -832,9 +880,9 @@ def __init__( self.smooth = smoothing self.mat = mat self.ham_rot = rotation - self.uaxis = uaxis - self.vaxis = vaxis - if disp_data: + self.uaxis = uaxis or UVAxis(0, 1, 0) + self.vaxis = vaxis or UVAxis(0, 0, -1) + if disp_data is not None: self.disp_power = utils.conv_int( disp_data.get('power', '_'), 4) self.disp_pos = Vec.from_str( @@ -899,8 +947,8 @@ def parse(vmf_file, tree): des_id=side_id, disp_data=disp_data, mat=tree['material', ''], - uaxis=tree['uaxis', '[0 1 0 0] 0.25'], - vaxis=tree['vaxis', '[0 0 -1 0] 0.25'], + uaxis=UVAxis.parse(tree['uaxis', '[0 1 0 0] 0.25']), + vaxis=UVAxis.parse(tree['vaxis', '[0 0 -1 0] 0.25']), rotation=utils.conv_int( tree['rotation', '0']), lightmap=utils.conv_int( @@ -920,7 +968,7 @@ def copy(self, des_id=-1): disp_data['subdiv'] = self.disp_is_subdiv disp_data['allowed_verts'] = self.disp_allowed_verts else: - disp_data = {} + disp_data = None return Side( self.map, @@ -928,8 +976,8 @@ def copy(self, des_id=-1): des_id=des_id, mat=self.mat, rotation=self.ham_rot, - uaxis=self.uaxis, - vaxis=self.vaxis, + uaxis=self.uaxis.copy(), + vaxis=self.vaxis.copy(), smoothing=self.smooth, lightmap=self.lightmap, disp_data=disp_data, @@ -943,8 +991,8 @@ def export(self, buffer, ind=''): pl_str = ('(' + p.join(' ') + ')' for p in self.planes) buffer.write(ind + '\t"plane" "' + ' '.join(pl_str) + '"\n') buffer.write(ind + '\t"material" "' + self.mat + '"\n') - buffer.write(ind + '\t"uaxis" "' + self.uaxis + '"\n') - buffer.write(ind + '\t"vaxis" "' + self.vaxis + '"\n') + buffer.write(ind + '\t"uaxis" "' + str(self.uaxis) + '"\n') + buffer.write(ind + '\t"vaxis" "' + str(self.vaxis) + '"\n') buffer.write(ind + '\t"rotation" "' + str(self.ham_rot) + '"\n') buffer.write(ind + '\t"lightmapscale" "' + str(self.lightmap) + '"\n') buffer.write(ind + '\t"smoothing_groups" "' + str(self.smooth) + '"\n') @@ -1041,6 +1089,16 @@ def normal(self): return point_2.cross(point_1).norm() + def scale(self, value): + self.uaxis.scale = value + self.vaxis.scale = value + scale = property(fset=scale, doc='Set botth scale attributes easily.') + + def offset(self, value): + self.uaxis.offset = value + self.vaxis.offset = value + offset = property(fset=offset, doc='Set botth scale attributes easily.') + class Entity: """A representation of either a point or brush entity. From 41d6118af8f80b4d0351c19c3fc217b75b9bff62 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sat, 29 Aug 2015 15:34:28 +1000 Subject: [PATCH 28/30] Allow items to be visible if they don't have any tags or authors --- src/UI.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/UI.py b/src/UI.py index 8c9b13de2..de8670865 100644 --- a/src/UI.py +++ b/src/UI.py @@ -1053,14 +1053,14 @@ def update_filters(): # deselected (show_wip or not item.item.is_wip) # Visible if any of the author and tag checkboxes are checked - and any( + and (any( FilterVars['author'][auth.casefold()].get() for auth in item.item.authors - ) - and any( + ) or not item.item.authors) # Show if no authors + and (any( FilterVars['tags'][tag.casefold()].get() for tag in item.item.tags - ) + ) or not item.item.tags) # Show if no tags # The package is selected and FilterVars['package'][item.item.pak_id].get() # Items like the elevator that need the unlocked stylevar @@ -1439,7 +1439,6 @@ def filter_all_callback(col): command=update_filters, variable=FilterVars[cat][filt_id], ) - FilterBoxes[cat][filt_id]['variable'] = FilterVars[cat][filt_id] FilterBoxes[cat][filt_id].grid( row=ind+2, column=0, From fc427b61294e836fd6b3a912bf19d4d26f2f95b9 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sat, 29 Aug 2015 15:35:24 +1000 Subject: [PATCH 29/30] Stop spamming texture axes, and detect more instance types for game mode --- src/vbsp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vbsp.py b/src/vbsp.py index 8eb4d19d0..c7d71c447 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -695,6 +695,7 @@ def get_map_info(): else: IS_PREVIEW = not utils.conv_bool(item.fixup['no_player_start']) if file in file_sp_exit_corr: + GAME_MODE = 'SP' exit_origin = Vec.from_str(item['origin']) if override_sp_exit == 0: utils.con_log( @@ -706,6 +707,7 @@ def get_map_info(): 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: + GAME_MODE = 'SP' entry_origin = Vec.from_str(item['origin']) if override_sp_entry == 0: utils.con_log( @@ -1144,8 +1146,6 @@ def fix_squarebeams(face, rotate, reset_offset: bool, scale: float): targ.offset = 0 targ.scale = scale - utils.con_log(face.uaxis, face.vaxis) - def change_brush(): """Alter all world/detail brush textures to use the configured ones.""" From cf47df10e3f0efbd98ba9173a93faff36b5b11a4 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sat, 29 Aug 2015 15:36:01 +1000 Subject: [PATCH 30/30] AddBrush Result This spawns in a brush at the specified location, with appropriate wall, floor and ceiling textures. --- src/conditions.py | 116 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/src/conditions.py b/src/conditions.py index d1491425a..cdddf4e5c 100644 --- a/src/conditions.py +++ b/src/conditions.py @@ -2107,3 +2107,119 @@ def res_set_texture(inst, res): # Don't allow this to get overwritten later. vbsp.IGNORED_FACES.add(brush.face) + + +@make_result('AddBrush') +def res_add_brush(inst, res): + """Spawn in a brush at the indicated points. + + - point1 and point2 are locations local to the instance, with '0 0 0' + as the floor-position. + - type is either 'black' or 'white'. + - detail should be set to True/False. If true the brush will be a + func_detail instead of a world brush. + + The sides will be textured with 1x1, 2x2 or 4x4 wall, ceiling and floor + textures as needed. + """ + import vbsp + + point1 = Vec.from_str(res['point1']) + point2 = Vec.from_str(res['point2']) + + point1.z -= 64 # Offset to the location of the floor + point2.z -= 64 + + point1.rotate_by_str(inst['angles']) # Rotate to match the instance + point2.rotate_by_str(inst['angles']) + + origin = Vec.from_str(inst['origin']) + point1 += origin # Then offset to the location of the instance + point2 += origin + + tex_type = res['type', None] + if tex_type not in ('white', 'black'): + utils.con_log( + 'AddBrush: "{}" is not a valid brush ' + 'color! (white or black)'.format(tex_type) + ) + tex_type = 'black' + + # We need to rescale black walls and ceilings + rescale = vbsp.get_bool_opt('random_blackwall_scale') and tex_type == 'black' + + dim = point2 - point1 + dim.max(-dim) + + # Figure out what grid size and scale is needed + # Check the dimensions in two axes to figure out the largest + # tile size that can fit in it. + x_maxsize = min(dim.y, dim.z) + y_maxsize = min(dim.x, dim.z) + if x_maxsize <= 32: + x_grid = '4x4' + x_scale = 0.25 + elif x_maxsize <= 64: + x_grid = '2x2' + x_scale = 0.5 + else: + x_grid = 'wall' + x_scale = 1 + + if y_maxsize <= 32: + y_grid = '4x4' + y_scale = 0.25 + elif y_maxsize <= 64: + y_grid = '2x2' + y_scale = 0.5 + else: + y_grid = 'wall' + y_scale = 1 + + grid_offset = (origin // 128) + + # All brushes in each grid have the same textures for each side. + random.seed(grid_offset.join(' ') + '-partial_block') + + solids = VMF.make_prism(point1, point2) + ':type solids: VLib.PrismFace' + + # Ensure the faces aren't re-textured later + vbsp.IGNORED_FACES.update(solids.solid.sides) + + solids.north.mat = vbsp.get_tex(tex_type + '.' + y_grid) + solids.south.mat = vbsp.get_tex(tex_type + '.' + y_grid) + solids.east.mat = vbsp.get_tex(tex_type + '.' + x_grid) + solids.west.mat = vbsp.get_tex(tex_type + '.' + x_grid) + solids.top.mat = vbsp.get_tex(tex_type + '.floor') + solids.bottom.mat = vbsp.get_tex(tex_type + '.ceiling') + + if rescale: + z_maxsize = min(dim.x, dim.y) + # randomised black wall scale applies to the ceiling too + if z_maxsize <= 32: + z_scale = 0.25 + elif z_maxsize <= 64: + z_scale = random.choice((0.5, 0.5, 0.25)) + else: + z_scale = random.choice((1, 1, 0.5, 0.5, 0.25)) + else: + z_scale = 0.25 + + if rescale: + solids.north.scale = y_scale + solids.south.scale = y_scale + solids.east.scale = x_scale + solids.west.scale = x_scale + solids.bottom.scale = z_scale + + if utils.conv_bool(res['detail', False], False): + # Add the brush to a func_detail entity + VMF.create_ent( + classname='func_detail' + ).solids = [ + solids.solid + ] + else: + # Add to the world + VMF.add_brush(solids.solid)