def parse(cls, conf: Property) -> 'FizzlerBrush': """Parse from a config file.""" if 'side_color' in conf: side_color = conf.vec('side_color') else: side_color = None outputs = [ Output.parse(prop) for prop in conf.find_children('Outputs') ] textures = {} for group in TexGroup: textures[group] = conf['tex_' + group.value, None] keys = { prop.name: prop.value for prop in conf.find_children('keys') } local_keys = { prop.name: prop.value for prop in conf.find_children('localkeys') } if 'classname' not in keys: raise ValueError( 'Fizzler Brush "{}" does not have a classname!'.format( conf['name'], ) ) return FizzlerBrush( name=conf['name'], textures=textures, keys=keys, local_keys=local_keys, outputs=outputs, thickness=conf.float('thickness', 2.0), stretch_center=conf.bool('stretch_center', True), side_color=side_color, singular=conf.bool('singular'), mat_mod_name=conf['mat_mod_name', None], mat_mod_var=conf['mat_mod_var', None], set_axis_var=conf.bool('set_axis_var'), )
def parse(cls, conf: Property) -> 'FizzlerBrush': """Parse from a config file.""" if 'side_color' in conf: side_color = conf.vec('side_color') else: side_color = None outputs = [ Output.parse(prop) for prop in conf.find_children('Outputs') ] textures = {} for group in TexGroup: textures[group] = conf['tex_' + group.value, None] keys = { prop.name: prop.value for prop in conf.find_children('keys') } local_keys = { prop.name: prop.value for prop in conf.find_children('localkeys') } if 'classname' not in keys: raise ValueError( 'Fizzler Brush "{}" does not have a classname!'.format( conf['name'], ) ) return FizzlerBrush( name=conf['name'], textures=textures, keys=keys, local_keys=local_keys, outputs=outputs, thickness=conf.float('thickness', 2.0), stretch_center=conf.bool('stretch_center', True), side_color=side_color, singular=conf.bool('singular'), mat_mod_name=conf['mat_mod_name', None], mat_mod_var=conf['mat_mod_var', None], set_axis_var=conf.bool('set_axis_var'), )
def res_add_global_inst(res: Property): """Add one instance in a location. Options: allow_multiple: Allow multiple copies of this instance. If 0, the instance will not be added if it was already added. name: The targetname of the instance. IF blank, the instance will be given a name of the form 'inst_1234'. file: The filename for the instance. Angles: The orientation of the instance (defaults to '0 0 0'). Origin: The location of the instance (defaults to '0 0 -10000'). Fixup_style: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. """ if res.value is not None: if res.bool('allow_multiple') or res['file'] not in GLOBAL_INSTANCES: # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = vbsp.VMF.create_ent( classname="func_instance", targetname=res['name', ''], file=instanceLocs.resolve_one(res['file'], error=True), angles=res['angles', '0 0 0'], origin=res['position', '0 0 -10000'], fixup_style=res['fixup_style', '0'], ) GLOBAL_INSTANCES.add(res['file']) if new_inst['targetname'] == '': new_inst['targetname'] = "inst_" new_inst.make_unique() return RES_EXHAUSTED
def parse(prop: Property): """Parse from property values. The value can be in four forms: "prop" "material" "prop" "<scale>|material" "prop" "<scale>|material|static" "prop" { "tex" "<mat>" "scale" "<scale>" "static" "<is_static>" } """ if prop.has_children(): tex = prop['tex'] scale = prop.float('scale', 0.25) static = prop.bool('static') else: vals = prop.value.split('|') opts = () scale_str = '0.25' if len(vals) == 2: scale_str, tex = vals elif len(vals) > 2: scale_str, tex, *opts = vals else: # Unpack to ensure it only has 1 section [tex] = vals scale = conv_float(scale_str, 0.25) static = 'static' in opts return AntTex(tex, scale, static)
def res_replace_instance(vmf: VMF, inst: Entity, res: Property): """Replace an instance with another entity. `keys` and `localkeys` defines the new keyvalues used. `targetname` and `angles` are preset, and `origin` will be used to offset the given amount from the current location. If `keep_instance` is true, the instance entity will be kept instead of removed. """ origin = Vec.from_str(inst['origin']) angles = Angle.from_str(inst['angles']) if not res.bool('keep_instance'): inst.remove() # Do this first to free the ent ID, so the new ent has # the same one. # We copy to allow us to still access the $fixups and other values. new_ent = inst.copy(des_id=inst.id) new_ent.clear_keys() # Ensure there's a classname, just in case. new_ent['classname'] = 'info_null' vmf.add_ent(new_ent) conditions.set_ent_keys(new_ent, inst, res) new_ent['origin'] = Vec.from_str(new_ent['origin']) @ angles + origin new_ent['angles'] = angles new_ent['targetname'] = inst['targetname']
def parse(prop: Property): """Parse from property values. The value can be in four forms: "prop" "material" "prop" "<scale>|material" "prop" "<scale>|material|static" "prop" { "tex" "<mat>" "scale" "<scale>" "static" "<is_static>" } """ if prop.has_children(): tex = prop['tex'] scale = prop.float('scale', 0.25) static = prop.bool('static') else: vals = prop.value.split('|') opts = () scale_str = '0.25' if len(vals) == 2: scale_str, tex = vals elif len(vals) > 2: scale_str, tex, *opts = vals else: # Unpack to ensure it only has 1 section [tex] = vals scale = conv_float(scale_str, 0.25) static = 'static' in opts return AntTex(tex, scale, static)
def res_calc_opposite_wall_dist(inst: Entity, res: Property): """Calculate the distance between this item and the opposing wall. The value is stored in the `$var` specified by the property value. Alternately it is set by `ResultVar`, and `offset` adds or subtracts to the value. `GooCollide` means that it will stop when goo is found, otherwise it is ignored. `GooAdjust` means additionally if the space is goo, the distance will be modified so that it specifies the surface of the goo. """ if res.has_children(): result_var = res['ResultVar'] dist_off = res.float('offset') collide_goo = res.bool('GooCollide') adjust_goo = res.bool('GooAdjust') else: result_var = res.value dist_off = 0 collide_goo = adjust_goo = False origin = Vec.from_str(inst['origin']) normal = Vec(z=1).rotate_by_str(inst['angles']) mask = [ brushLoc.Block.SOLID, brushLoc.Block.EMBED, brushLoc.Block.PIT_BOTTOM, brushLoc.Block.PIT_SINGLE, ] # Only if actually downward. if normal == (0, 0, -1) and collide_goo: mask.append(brushLoc.Block.GOO_TOP) mask.append(brushLoc.Block.GOO_SINGLE) opposing_pos = brushLoc.POS.raycast_world( origin, normal, mask, ) if adjust_goo and brushLoc.POS['world':opposing_pos + 128 * normal].is_goo: # If the top is goo, adjust so the 64 below is the top of the goo. dist_off += 32 inst.fixup[result_var] = (origin - opposing_pos).mag() + dist_off
def res_calc_opposite_wall_dist(inst: Entity, res: Property): """Calculate the distance between this item and the opposing wall. The value is stored in the `$var` specified by the property value. Alternately it is set by `ResultVar`, and `offset` adds or subtracts to the value. `GooCollide` means that it will stop when goo is found, otherwise it is ignored. `GooAdjust` means additionally if the space is goo, the distance will be modified so that it specifies the surface of the goo. """ if res.has_children(): result_var = res['ResultVar'] dist_off = res.float('offset') collide_goo = res.bool('GooCollide') adjust_goo = res.bool('GooAdjust') else: result_var = res.value dist_off = 0 collide_goo = adjust_goo = False origin = Vec.from_str(inst['origin']) normal = Vec(z=1).rotate_by_str(inst['angles']) mask = [ brushLoc.Block.SOLID, brushLoc.Block.EMBED, brushLoc.Block.PIT_BOTTOM, brushLoc.Block.PIT_SINGLE, ] # Only if actually downward. if normal == (0, 0, -1) and collide_goo: mask.append(brushLoc.Block.GOO_TOP) mask.append(brushLoc.Block.GOO_SINGLE) opposing_pos = brushLoc.POS.raycast_world( origin, normal, mask, ) if adjust_goo and brushLoc.POS['world': opposing_pos + 128*normal].is_goo: # If the top is goo, adjust so the 64 below is the top of the goo. dist_off += 32 inst.fixup[result_var] = (origin - opposing_pos).mag() + dist_off
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property): """Create the spawn point for ATLAS in the entry corridor. It produces either an instance or the normal spawn entity. This is required since ATLAS may need to have the paint gun logic. The two parameters `origin` and `facing` must be set to determine the required position. If `global` is set, the spawn point will be absolute instead of relative to the current instance. """ if vbsp.GAME_MODE != 'COOP': return RES_EXHAUSTED is_tag = options.get(str, 'game_id') == utils.STEAM_IDS['TAG'] origin = res.vec('origin') normal = res.vec('facing', z=1) # Some styles might want to ignore the instance we're running on. if not res.bool('global'): origin = origin.rotate_by_str(inst['angles']) normal = normal.rotate_by_str(inst['angles']) origin += Vec.from_str(inst['origin']) angles = normal.to_angle() if is_tag: vmf.create_ent( classname='func_instance', targetname='paint_gun', origin=origin - (0, 0, 16), angles=angles, # Generated by the BEE2 app. file='instances/bee2/tag_coop_gun.vmf', ) # Blocks ATLAS from having a gun vmf.create_ent( classname='info_target', targetname='supress_blue_portalgun_spawn', origin=origin, angles='0 0 0', ) # Allows info_target to work vmf.create_ent( classname='env_global', targetname='no_spawns', globalstate='portalgun_nospawn', initialstate=1, spawnflags=1, # Use initial state origin=origin, ) vmf.create_ent( classname='info_coop_spawn', targetname='@coop_spawn_blue', ForceGunOnSpawn=int(not is_tag), origin=origin, angles=angles, enabled=1, StartingTeam=3, # ATLAS ) return RES_EXHAUSTED
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property): """Create the spawn point for ATLAS in the entry corridor. It produces either an instance or the normal spawn entity. This is required since ATLAS may need to have the paint gun logic. The two parameters `origin` and `facing` must be set to determine the required position. If `global` is set, the spawn point will be absolute instead of relative to the current instance. """ if vbsp.GAME_MODE != 'COOP': return RES_EXHAUSTED is_tag = vbsp_options.get(str, 'game_id') == utils.STEAM_IDS['TAG'] origin = res.vec('origin') normal = res.vec('facing', z=1) # Some styles might want to ignore the instance we're running on. if not res.bool('global'): origin = origin.rotate_by_str(inst['angles']) normal = normal.rotate_by_str(inst['angles']) origin += Vec.from_str(inst['origin']) angles = normal.to_angle() if is_tag: vmf.create_ent( classname='func_instance', targetname='paint_gun', origin=origin - (0, 0, 16), angles=angles, # Generated by the BEE2 app. file='instances/bee2/tag_coop_gun.vmf', ) # Blocks ATLAS from having a gun vmf.create_ent( classname='info_target', targetname='supress_blue_portalgun_spawn', origin=origin, angles='0 0 0', ) # Allows info_target to work vmf.create_ent( classname='env_global', targetname='no_spawns', globalstate='portalgun_nospawn', initialstate=1, spawnflags=1, # Use initial state origin=origin, ) vmf.create_ent( classname='info_coop_spawn', targetname='@coop_spawn_blue', ForceGunOnSpawn=int(not is_tag), origin=origin, angles=angles, enabled=1, StartingTeam=3, # ATLAS ) return RES_EXHAUSTED
def load_handler(props: Property) -> None: """Load compiler options from the palette.""" chosen_thumb.set(props['sshot_type', chosen_thumb.get()]) cleanup_screenshot.set( props.bool('sshot_cleanup', cleanup_screenshot.get())) if 'sshot_data' in props: screenshot_parts = b'\n'.join([ prop.value.encode('ascii') for prop in props.find_children('sshot_data') ]) screenshot_data = base64.decodebytes(screenshot_parts) with atomic_write(SCREENSHOT_LOC, mode='wb', overwrite=True) as f: f.write(screenshot_data) # Refresh these. set_screen_type() set_screenshot() start_in_elev.set(props.bool('spawn_elev', start_in_elev.get())) try: player_mdl = props['player_model'] except LookupError: pass else: player_model_var.set(PLAYER_MODELS[player_mdl]) COMPILE_CFG['General']['player_model'] = player_mdl VOICE_PRIORITY_VAR.set( props.bool('voiceline_priority', VOICE_PRIORITY_VAR.get())) corr_prop = props.find_block('corridor', or_blank=True) for group, win in CORRIDOR.items(): try: sel_id = corr_prop[group] except LookupError: "No config option, ok." else: win.sel_item_id(sel_id) COMPILE_CFG['Corridor'][ group] = '0' if sel_id == '<NONE>' else sel_id COMPILE_CFG.save_check() return None
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property): """Create the spawn point for ATLAS, in Aperture Tag. This creates an instance with the desired orientation. The two parameters 'origin' and 'angles' must be set. """ if vbsp.GAME_MODE != 'COOP': return RES_EXHAUSTED is_tag = vbsp_options.get(str, 'game_id') == utils.STEAM_IDS['TAG'] origin = res.vec('origin') normal = res.vec('facing', z=1) # Some styles might want to ignore the instance we're running on. if not res.bool('global'): origin = origin.rotate_by_str(inst['angles']) normal = normal.rotate_by_str(inst['angles']) origin += Vec.from_str(inst['origin']) angles = normal.to_angle() if is_tag: vmf.create_ent( classname='func_instance', targetname='paint_gun', origin=origin - (0, 0, 16), angles=angles, # Generated by the BEE2 app. file='instances/bee2/tag_coop_gun.vmf', ) # Blocks ATLAS from having a gun vmf.create_ent( classname='info_target', targetname='supress_blue_portalgun_spawn', origin=origin, angles='0 0 0', ) # Allows info_target to work vmf.create_ent( classname='env_global', targetname='no_spawns', globalstate='portalgun_nospawn', initialstate=1, spawnflags=1, # Use initial state origin=origin, ) vmf.create_ent( classname='info_coop_spawn', targetname='@coop_spawn_blue', ForceGunOnSpawn=int(not is_tag), origin=origin, angles=angles, enabled=1, StartingTeam=3, # ATLAS ) return RES_EXHAUSTED
def res_cust_antline_setup(res: Property) -> Callable[[Entity], None]: """Customise the output antlines. Options: * `wall`: The configuration for antlines on walls. Same as global style options. * `floor`: The configuration for floor/ceiling antlines. If not provided, this is assumed to be the same as `wall`. * `remove_signs`: If true, remove the indicator signs. * `toggle_var`: If set, this item controls the toggle state fully of its antlines. This is a fixup var which will be set to the name of the overlays, for user control. """ wall_style: antlines.AntType | None floor_type: antlines.AntType | None if 'wall' in res: wall_style = antlines.AntType.parse(res.find_key('wall')) else: wall_style = None if 'floor' in res: floor_style = antlines.AntType.parse(res.find_key('floor')) else: floor_style = wall_style remove_signs = res.bool('remove_signs') toggle_var = res['toggle_var', ''] def change_antlines(inst: Entity) -> None: """Change the antlines of an item.""" item = connections.ITEMS[inst['targetname']] if wall_style is not None: item.ant_wall_style = wall_style if floor_style is not None: item.ant_floor_style = floor_style if remove_signs: for sign in item.ind_panels: sign.remove() item.ind_panels.clear() if toggle_var: item.ant_toggle_var = toggle_var return change_antlines
def res_cust_antline_setup(res: Property): if 'wall' in res: wall_type = antlines.AntType.parse(res.find_key('wall')) else: wall_type = None if 'floor' in res: floor_type = antlines.AntType.parse(res.find_key('floor')) else: floor_type = wall_type return ( wall_type, floor_type, res.bool('remove_signs'), res['toggle_var', ''], )
def res_cust_antline_setup(res: Property): if 'wall' in res: wall_type = antlines.AntType.parse(res.find_key('wall')) else: wall_type = None if 'floor' in res: floor_type = antlines.AntType.parse(res.find_key('floor')) else: floor_type = wall_type return ( wall_type, floor_type, res.bool('remove_signs'), res['toggle_var', ''], )
def flag_angles(flag: Property) -> Callable[[Entity], bool]: """Check that a instance is pointed in a direction. The value should be either just the angle to check, or a block of options: - `direction`: A unit vector (XYZ value) pointing in a direction, or some keywords: `+z`, `-y`, `N`/`S`/`E`/`W`, `up`/`down`, `floor`/`ceiling`, or `walls` for any wall side. - `From_dir`: The direction the unrotated instance is pointed in. This lets the flag check multiple directions. - `Allow_inverse`: If true, this also returns True if the instance is pointed the opposite direction . """ if flag.has_children(): targ_angle = flag['direction', '0 0 0'] from_dir = flag['from_dir', '0 0 1'] if from_dir.casefold() in DIRECTIONS: from_dir = Vec(DIRECTIONS[from_dir.casefold()]) else: from_dir = Vec.from_str(from_dir, 0, 0, 1) allow_inverse = flag.bool('allow_inverse') else: targ_angle = flag.value from_dir = Vec(0, 0, 1) allow_inverse = False try: normal = DIRECTIONS[targ_angle.casefold()] except KeyError: normal = Vec.from_str(targ_angle) def check_orient(inst: Entity) -> bool: """Check the orientation against the instance.""" inst_normal = from_dir @ Angle.from_str(inst['angles']) if normal == 'WALL': # Special case - it's not on the floor or ceiling return abs(inst_normal.z) < 1e-6 else: return inst_normal == normal or ( allow_inverse and -inst_normal == normal ) return check_orient
def res_global_input_setup(res: Property) -> tuple[str, Output]: """Pre-parse the global input.""" if res.has_children(): name = res['name', ''] if not name and res.bool('alsoonload'): name = ON_LOAD inp_name, inp_command = Output.parse_name(res['input']) return name, Output( out=res['output', 'OnTrigger'], targ=res['target', ''], inp=inp_command, inst_in=inp_name, delay=srctools.conv_float(res['delay', '']), param=res['param', ''], ) else: out = Output.parse(res) out.output = '' # Don't need to store GlobalInput... return '', out
def res_add_global_inst(vmf: VMF, res: Property): """Add one instance in a specific location. Options: - `allow_multiple`: Allow multiple copies of this instance. If 0, the instance will not be added if it was already added. - `name`: The targetname of the instance. If blank, the instance will be given a name of the form `inst_1234`. - `file`: The filename for the instance. - `angles`: The orientation of the instance (defaults to `0 0 0`). - `fixup_style`: The Fixup style for the instance. `0` (default) is Prefix, `1` is Suffix, and `2` is None. - `position`: The location of the instance. If not set, it will be placed in a 128x128 nodraw room somewhere in the map. Objects which can interact with nearby object should not be placed there. """ if not res.has_children(): res = Property('AddGlobal', [Property('File', res.value)]) file = instanceLocs.resolve_one(res['file'], error=True) if res.bool('allow_multiple') or file.casefold() not in conditions.GLOBAL_INSTANCES: # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = vmf.create_ent( classname="func_instance", targetname=res['name', ''], file=file, angles=res['angles', '0 0 0'], fixup_style=res['fixup_style', '0'], ) try: new_inst['origin'] = res['position'] except IndexError: new_inst['origin'] = options.get(Vec, 'global_ents_loc') conditions.GLOBAL_INSTANCES.add(file.casefold()) conditions.ALL_INST.add(file.casefold()) if new_inst['targetname'] == '': new_inst['targetname'] = "inst_" new_inst.make_unique() return conditions.RES_EXHAUSTED
def res_get_item_config(inst: Entity, res: Property): """Check if an item config panel value matches another value. ID is the ID of the group. Name is the name of the widget. If UseTimer is true, it uses $timer_delay to choose the value to use. Value is the value to compare to. """ group_id = res['ID'] wid_name = res['Name'].casefold() desired_value = res['Value'] if res.bool('UseTimer'): timer_delay = inst.fixup.int('$timer_delay') else: timer_delay = None conf = vbsp_options.get_itemconf((group_id, wid_name), None, timer_delay) if conf is None: # Doesn't exist return False return conf == desired_value
def flag_angles(inst: Entity, flag: Property): """Check that a instance is pointed in a direction. The value should be either just the angle to check, or a block of options: - `direction`: A unit vector (XYZ value) pointing in a direction, or some keywords: `+z`, `-y`, `N`/`S`/`E`/`W`, `up`/`down`, `floor`/`ceiling`, or `walls` for any wall side. - `From_dir`: The direction the unrotated instance is pointed in. This lets the flag check multiple directions. - `Allow_inverse`: If true, this also returns True if the instance is pointed the opposite direction . """ angle = inst['angles', '0 0 0'] if flag.has_children(): targ_angle = flag['direction', '0 0 0'] from_dir = flag['from_dir', '0 0 1'] if from_dir.casefold() in DIRECTIONS: from_dir = Vec(DIRECTIONS[from_dir.casefold()]) else: from_dir = Vec.from_str(from_dir, 0, 0, 1) allow_inverse = flag.bool('allow_inverse') else: targ_angle = flag.value from_dir = Vec(0, 0, 1) allow_inverse = False normal = DIRECTIONS.get(targ_angle.casefold(), None) if normal is None: return False # If it's not a special angle, # so it failed the exact match inst_normal = from_dir.rotate_by_str(angle) if normal == 'WALL': # Special case - it's not on the floor or ceiling return not (inst_normal == (0, 0, 1) or inst_normal == (0, 0, -1)) else: return inst_normal == normal or (allow_inverse and -inst_normal == normal)
def res_match_item_config(inst: Entity, res: Property) -> bool: """Check if an Item Config Panel value matches another value. * `ID` is the ID of the group. * `Name` is the name of the widget. * If `UseTimer` is true, it uses `$timer_delay` to choose the value to use. * `Value` is the value to compare to. """ group_id = res['ID'] wid_name = res['Name'].casefold() desired_value = res['Value'] if res.bool('UseTimer'): timer_delay = inst.fixup.int('$timer_delay') else: timer_delay = None conf = options.get_itemconf((group_id, wid_name), None, timer_delay) if conf is None: # Doesn't exist return False return conf == desired_value
def res_match_item_config(inst: Entity, res: Property) -> bool: """Check if an item config panel value matches another value. * `ID` is the ID of the group. * `Name` is the name of the widget. * If `UseTimer` is true, it uses `$timer_delay` to choose the value to use. * `Value` is the value to compare to. """ group_id = res['ID'] wid_name = res['Name'].casefold() desired_value = res['Value'] if res.bool('UseTimer'): timer_delay = inst.fixup.int('$timer_delay') else: timer_delay = None conf = vbsp_options.get_itemconf((group_id, wid_name), None, timer_delay) if conf is None: # Doesn't exist return False return conf == desired_value
def res_get_item_config(inst: Entity, res: Property): """Load a config from the item config panel onto a fixup. ID is the ID of the group. Name is the name of the widget, and resultVar is the location to store. If UseTimer is true, it uses $timer_delay to choose the value to use. Default is the default value, if the config isn't found. """ group_id = res['ID'] wid_name = res['Name'] default = res['default'] if res.bool('UseTimer'): timer_delay = inst.fixup.int('$timer_delay') else: timer_delay = None inst.fixup[res['ResultVar']] = vbsp_options.get_itemconf( (group_id, wid_name), default, timer_delay, )
def res_add_global_inst(res: Property): """Add one instance in a specific location. Options: allow_multiple: Allow multiple copies of this instance. If 0, the instance will not be added if it was already added. name: The targetname of the instance. IF blank, the instance will be given a name of the form 'inst_1234'. file: The filename for the instance. Angles: The orientation of the instance (defaults to '0 0 0'). Fixup_style: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. Position: The location of the instance. If not set, it will be placed in a 128x128 nodraw room somewhere in the map. Objects which can interact with nearby object should not be placed there. """ if not res.has_children(): res = Property('AddGlobal', [Property('File', res.value)]) if res.bool('allow_multiple') or res['file'] not in GLOBAL_INSTANCES: # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = vbsp.VMF.create_ent( classname="func_instance", targetname=res['name', ''], file=instanceLocs.resolve_one(res['file'], error=True), angles=res['angles', '0 0 0'], fixup_style=res['fixup_style', '0'], ) try: new_inst['origin'] = res['position'] except IndexError: new_inst['origin'] = vbsp_options.get(Vec, 'global_ents_loc') GLOBAL_INSTANCES.add(res['file']) if new_inst['targetname'] == '': new_inst['targetname'] = "inst_" new_inst.make_unique() return RES_EXHAUSTED
def res_add_global_inst(res: Property): """Add one instance in a specific location. Options: `allow_multiple`: Allow multiple copies of this instance. If 0, the instance will not be added if it was already added. `name`: The targetname of the instance. If blank, the instance will be given a name of the form `inst_1234`. `file`: The filename for the instance. `angles`: The orientation of the instance (defaults to `0 0 0`). `fixup_style`: The Fixup style for the instance. `0` (default) is Prefix, `1` is Suffix, and `2` is None. `position`: The location of the instance. If not set, it will be placed in a 128x128 nodraw room somewhere in the map. Objects which can interact with nearby object should not be placed there. """ if not res.has_children(): res = Property('AddGlobal', [Property('File', res.value)]) if res.bool('allow_multiple') or res['file'] not in GLOBAL_INSTANCES: # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = vbsp.VMF.create_ent( classname="func_instance", targetname=res['name', ''], file=instanceLocs.resolve_one(res['file'], error=True), angles=res['angles', '0 0 0'], fixup_style=res['fixup_style', '0'], ) try: new_inst['origin'] = res['position'] except IndexError: new_inst['origin'] = vbsp_options.get(Vec, 'global_ents_loc') GLOBAL_INSTANCES.add(res['file']) if new_inst['targetname'] == '': new_inst['targetname'] = "inst_" new_inst.make_unique() return RES_EXHAUSTED
def res_item_config_to_fixup(inst: Entity, res: Property): """Load a config from the item config panel onto a fixup. * `ID` is the ID of the group. * `Name` is the name of the widget. * `resultVar` is the location to store the value into. * If `UseTimer` is true, it uses `$timer_delay` to choose the value to use. * `Default` is the default value, if the config isn't found. """ group_id = res['ID'] wid_name = res['Name'] default = res['default'] if res.bool('UseTimer'): timer_delay = inst.fixup.int('$timer_delay') else: timer_delay = None inst.fixup[res['ResultVar']] = vbsp_options.get_itemconf( (group_id, wid_name), default, timer_delay, )
def res_item_config_to_fixup(inst: Entity, res: Property): """Load a config from the item config panel onto a fixup. * `ID` is the ID of the group. * `Name` is the name of the widget. * `resultVar` is the location to store the value into. * If `UseTimer` is true, it uses `$timer_delay` to choose the value to use. * `Default` is the default value, if the config isn't found. """ group_id = res['ID'] wid_name = res['Name'] default = res['default'] if res.bool('UseTimer'): timer_delay = inst.fixup.int('$timer_delay') else: timer_delay = None inst.fixup[res['ResultVar']] = vbsp_options.get_itemconf( (group_id, wid_name), default, timer_delay, )
def get_itemconf(inst: Entity, res: Property) -> str | None: """Implement ItemConfig and GetItemConfig shared logic.""" timer_delay: int | None group_id = res['ID'] wid_name = inst.fixup.substitute(res['Name']).casefold() match = BRACE_RE.match(wid_name) if match is not None: # Match name[timer], after $fixup substitution. wid_name, timer_str = match.groups() # Should not fail, we matched it above. timer_delay = int(timer_str) elif res.bool('UseTimer'): LOGGER.warning( 'UseTimer is deprecated, use name = "{}[$timer_delay]".', wid_name, ) timer_delay = inst.fixup.int('$timer_delay') else: timer_delay = None return options.get_itemconf((group_id, wid_name), None, timer_delay)
def res_piston_plat_setup(res: Property): # Allow reading instances direct from the ID. # But use direct ones first. item_id = res['itemid', None] inst = {} for name in INST_NAMES: if name in res: lookup = res[name] if lookup == '': # Special case, allow blank for no instance. inst[name] = '' continue elif item_id is not None: lookup = '<{}:bee2_pist_{}>'.format(item_id, name) else: raise ValueError('No "{}" specified!'.format(name)) inst[name] = resolve_single(lookup, error=True) template = template_brush.get_template(res['template']) visgroup_names = [ res['visgroup_1', 'pist_1'], res['visgroup_2', 'pist_2'], res['visgroup_3', 'pist_3'], res['visgroup_top', 'pist_4'], ] return ( template, visgroup_names, inst, res.bool('has_dn_fizz'), res['auto_var', ''], res['color_var', ''], res['source_ent', ''], res['snd_start', ''], res['snd_loop', ''], res['snd_stop', ''], )
def res_track_plat(vmf: VMF, res: Property): """Logic specific to Track Platforms. This allows switching the instances used depending on if the track is horizontal or vertical and sets the track targetnames to a useful value. This should be run unconditionally, not once per item. Values: * `orig_item`: The "<ITEM_ID>" for the track platform, with angle brackets. This is used to determine all the instance filenames. * `single_plat`: An instance used for the entire platform, if it's one rail long (and therefore can't move). * `track_name`: If set, rename track instances following the pattern `plat_name-track_nameXX`. Otherwise all tracks will receive the name of the platform. * `plat_suffix`: If set, add a `_vert` or `_horiz` suffix to the platform. * `plat_var`: If set, save the orientation (`vert`/`horiz`) to the provided $fixup variable. * `track_var`: If set, save `N`, `S`, `E`, or `W` to the provided $fixup variable to indicate the relative direction the top faces. """ # Get the instances from editoritems ( inst_bot_grate, inst_bottom, inst_middle, inst_top, inst_plat, inst_plat_oscil, inst_single ) = instanceLocs.resolve(res['orig_item']) single_plat_inst = instanceLocs.resolve_one(res['single_plat', '']) track_targets = res['track_name', ''] track_files = [inst_bottom, inst_middle, inst_top, inst_single] platforms = [inst_plat, inst_plat_oscil] # All the track_set in the map, indexed by origin track_instances = { Vec.from_str(inst['origin']).as_tuple(): inst for inst in vmf.by_class['func_instance'] if inst['file'].casefold() in track_files } LOGGER.debug('Track instances:') LOGGER.debug('\n'.join( '{!s}: {}'.format(k, v['file']) for k, v in track_instances.items() )) if not track_instances: return RES_EXHAUSTED # Now we loop through all platforms in the map, and then locate their # track_set for plat_inst in vmf.by_class['func_instance']: if plat_inst['file'].casefold() not in platforms: continue # Not a platform! LOGGER.debug('Modifying "' + plat_inst['targetname'] + '"!') plat_loc = Vec.from_str(plat_inst['origin']) # The direction away from the wall/floor/ceil normal = Vec(0, 0, 1).rotate_by_str( plat_inst['angles'] ) for tr_origin, first_track in track_instances.items(): if plat_loc == tr_origin: # Check direction if normal == Vec(0, 0, 1).rotate( *Vec.from_str(first_track['angles']) ): break else: raise Exception('Platform "{}" has no track!'.format( plat_inst['targetname'] )) track_type = first_track['file'].casefold() if track_type == inst_single: # Track is one block long, use a single-only instance and # remove track! plat_inst['file'] = single_plat_inst first_track.remove() continue # Next platform track_set = set() # type: Set[Entity] if track_type == inst_top or track_type == inst_middle: # search left track_scan( track_set, track_instances, first_track, middle_file=inst_middle, x_dir=-1, ) if track_type == inst_bottom or track_type == inst_middle: # search right track_scan( track_set, track_instances, first_track, middle_file=inst_middle, x_dir=+1, ) # Give every track a targetname matching the platform for ind, track in enumerate(track_set, start=1): if track_targets == '': track['targetname'] = plat_inst['targetname'] else: track['targetname'] = ( plat_inst['targetname'] + '-' + track_targets + str(ind) ) # Now figure out which way the track faces: # The direction of the platform surface facing = Vec(-1, 0, 0).rotate_by_str(plat_inst['angles']) # The direction horizontal track is offset uaxis = Vec(x=1).rotate_by_str(first_track['angles']) vaxis = Vec(y=1).rotate_by_str(first_track['angles']) if uaxis == facing: plat_facing = 'vert' track_facing = 'E' elif uaxis == -facing: plat_facing = 'vert' track_facing = 'W' elif vaxis == facing: plat_facing = 'horiz' track_facing = 'N' elif vaxis == -facing: plat_facing = 'horiz' track_facing = 'S' else: raise ValueError('Facing {} is not U({}) or V({})!'.format( facing, uaxis, vaxis, )) if res.bool('plat_suffix'): conditions.add_suffix(plat_inst, '_' + plat_facing) plat_var = res['plat_var', ''] if plat_var: plat_inst.fixup[plat_var] = plat_facing track_var = res['track_var', ''] if track_var: plat_inst.fixup[track_var] = track_facing for track in track_set: track.fixup.update(plat_inst.fixup) return RES_EXHAUSTED # Don't re-run
def res_conveyor_belt(vmf: VMF, inst: Entity, res: Property) -> None: """Create a conveyor belt. * Options: * `SegmentInst`: Generated at each square. (`track` is the name of the path to attach to.) * `TrackTeleport`: Set the track points so they teleport trains to the start. * `Speed`: The fixup or number for the train speed. * `MotionTrig`: If set, a trigger_multiple will be spawned that `EnableMotion`s weighted cubes. The value is the name of the relevant filter. * `EndOutput`: Adds an output to the last track. The value is the same as outputs in VMFs. `RotateSegments`: If true (default), force segments to face in the direction of movement. * `BeamKeys`: If set, a list of keyvalues to use to generate an env_beam travelling from start to end. The origin is treated specially - X is the distance from walls, y is the distance to the side, and z is the height. `RailTemplate`: A template for the track sections. This is made into a non-solid func_brush, combining all sections. * `NoPortalFloor`: If set, add a `func_noportal_volume` on the floor under the track. * `PaintFizzler`: If set, add a paint fizzler underneath the belt. """ move_dist = inst.fixup.int('$travel_distance') if move_dist <= 2: # There isn't room for a conveyor, so don't bother. inst.remove() return orig_orient = Matrix.from_angle(Angle.from_str(inst['angles'])) move_dir = Vec(1, 0, 0) @ Angle.from_str(inst.fixup['$travel_direction']) move_dir = move_dir @ orig_orient start_offset = inst.fixup.float('$starting_position') teleport_to_start = res.bool('TrackTeleport', True) segment_inst_file = instanceLocs.resolve_one(res['SegmentInst', '']) rail_template = res['RailTemplate', None] track_speed = res['speed', None] start_pos = Vec.from_str(inst['origin']) end_pos = start_pos + move_dist * move_dir if start_offset > 0: # If an oscillating platform, move to the closest side.. offset = start_offset * move_dir # The instance is placed this far along, so move back to the end. start_pos -= offset end_pos -= offset if start_offset > 0.5: # Swap the direction of movement.. start_pos, end_pos = end_pos, start_pos inst['origin'] = start_pos norm = orig_orient.up() if res.bool('rotateSegments', True): orient = Matrix.from_basis(x=move_dir, z=norm) inst['angles'] = orient.to_angle() else: orient = orig_orient # Add the EnableMotion trigger_multiple seen in platform items. # This wakes up cubes when it starts moving. motion_filter = res['motionTrig', None] # Disable on walls, or if the conveyor can't be turned on. if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0': motion_filter = None track_name = conditions.local_name(inst, 'segment_{}') rail_temp_solids = [] last_track = None # Place tracks at the top, so they don't appear inside wall sections. track_start: Vec = start_pos + 48 * norm track_end: Vec = end_pos + 48 * norm for index, pos in enumerate(track_start.iter_line(track_end, stride=128), start=1): track = vmf.create_ent( classname='path_track', targetname=track_name.format(index) + '-track', origin=pos, spawnflags=0, orientationtype=0, # Don't rotate ) if track_speed is not None: track['speed'] = track_speed if last_track: last_track['target'] = track['targetname'] if index == 1 and teleport_to_start: track['spawnflags'] = 16 # Teleport here.. last_track = track # Don't place at the last point - it doesn't teleport correctly, # and would be one too many. if segment_inst_file and pos != track_end: seg_inst = conditions.add_inst( vmf, targetname=track_name.format(index), file=segment_inst_file, origin=pos, angles=orient, ) seg_inst.fixup.update(inst.fixup) if rail_template: temp = template_brush.import_template( vmf, rail_template, pos, orient, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) rail_temp_solids.extend(temp.world) if rail_temp_solids: vmf.create_ent( classname='func_brush', origin=track_start, spawnflags=1, # Ignore +USE solidity=1, # Not solid vrad_brush_cast_shadows=1, drawinfastreflection=1, ).solids = rail_temp_solids if teleport_to_start: # Link back to the first track.. last_track['target'] = track_name.format(1) + '-track' # Generate an env_beam pointing from the start to the end of the track. try: beam_keys = res.find_key('BeamKeys') except LookupError: pass else: beam = vmf.create_ent(classname='env_beam') beam_off = beam_keys.vec('origin', 0, 63, 56) for prop in beam_keys: beam[prop.real_name] = prop.value # Localise the targetname so it can be triggered.. beam['LightningStart'] = beam['targetname'] = conditions.local_name( inst, beam['targetname', 'beam']) del beam['LightningEnd'] beam['origin'] = start_pos + Vec( -beam_off.x, beam_off.y, beam_off.z, ) @ orient beam['TargetPoint'] = end_pos + Vec( +beam_off.x, beam_off.y, beam_off.z, ) @ orient # Allow adding outputs to the last path_track. for prop in res.find_all('EndOutput'): output = Output.parse(prop) output.output = 'OnPass' output.inst_out = None output.comma_sep = False output.target = conditions.local_name(inst, output.target) last_track.add_out(output) if motion_filter is not None: motion_trig = vmf.create_ent( classname='trigger_multiple', targetname=conditions.local_name(inst, 'enable_motion_trig'), origin=start_pos, filtername=motion_filter, startDisabled=1, wait=0.1, ) motion_trig.add_out( Output('OnStartTouch', '!activator', 'ExitDisabledState')) # Match the size of the original... motion_trig.solids.append( vmf.make_prism( start_pos + Vec(72, -56, 58) @ orient, end_pos + Vec(-72, 56, 144) @ orient, mat=consts.Tools.TRIGGER, ).solid) if res.bool('NoPortalFloor'): # Block portals on the floor.. floor_noportal = vmf.create_ent( classname='func_noportal_volume', origin=track_start, ) floor_noportal.solids.append( vmf.make_prism( start_pos + Vec(-60, -60, -66) @ orient, end_pos + Vec(60, 60, -60) @ orient, mat=consts.Tools.INVISIBLE, ).solid) # A brush covering under the platform. base_trig = vmf.make_prism( start_pos + Vec(-64, -64, 48) @ orient, end_pos + Vec(64, 64, 56) @ orient, mat=consts.Tools.INVISIBLE, ).solid vmf.add_brush(base_trig) # Make a paint_cleanser under the belt.. if res.bool('PaintFizzler'): pfizz = vmf.create_ent( classname='trigger_paint_cleanser', origin=start_pos, ) pfizz.solids.append(base_trig.copy()) for face in pfizz.sides(): face.mat = consts.Tools.TRIGGER
def res_add_overlay_inst(inst: Entity, res: Property): """Add another instance on top of this one. If a single value, this sets only the filename. Values: `file`: The filename. `fixup_style`: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. `copy_fixup`: If true, all the $replace values from the original instance will be copied over. `move_outputs`: If true, outputs will be moved to this instance. `offset`: The offset (relative to the base) that the instance will be placed. Can be set to '<piston_top>' and '<piston_bottom>' to offset based on the configuration. '<piston_start>' will set it to the starting position, and '<piston_end>' will set it to the ending position. of piston platform handles. `angles`: If set, overrides the base instance angles. This does not affect the offset property. `fixup`/`localfixup`: Keyvalues in this block will be copied to the overlay entity. If the value starts with $, the variable will be copied over. If this is present, copy_fixup will be disabled. """ if not res.has_children(): # Use all the defaults. res = Property('AddOverlay', [ Property('File', res.value) ]) angle = res['angles', inst['angles', '0 0 0']] orig_name = conditions.resolve_value(inst, res['file', '']) filename = instanceLocs.resolve_one(orig_name) if not filename: if not res.bool('silentLookup'): LOGGER.warning('Bad filename for "{}" when adding overlay!', orig_name) # Don't bother making a overlay which will be deleted. return overlay_inst = vbsp.VMF.create_ent( classname='func_instance', targetname=inst['targetname', ''], file=filename, angles=angle, origin=inst['origin'], fixup_style=res['fixup_style', '0'], ) # Don't run if the fixup block exists.. if srctools.conv_bool(res['copy_fixup', '1']): if 'fixup' not in res and 'localfixup' not in res: # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value conditions.set_ent_keys(overlay_inst.fixup, inst, res, 'fixup') if res.bool('move_outputs', False): overlay_inst.outputs = inst.outputs inst.outputs = [] if 'offset' in res: overlay_inst['origin'] = conditions.resolve_offset(inst, res['offset']) return overlay_inst
def res_cust_fizzler(base_inst: Entity, res: Property): """Customises the various components of a custom fizzler item. This should be executed on the base instance. Brush and MakeLaserField are not permitted on laserfield barriers. When executed, the $is_laser variable will be set on the base. Options: * 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 deleted.) This cannot be used on laserfields. * 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. * Thickness will change the thickness of the fizzler if set. By default it is 2 units thick. * Outputs is a block of outputs (laid out like in VMFs). The targetnames will be localised to the instance. * MergeBrushes, if true will merge this brush set into one entity for each fizzler. This is useful for non-fizzlers to reduce the entity count. * SimplifyBrush, if true will merge the three parts into one brush. All sides will receive the "nodraw" texture at 0.25 scale. * MaterialModify generates material_modify_controls to control the brush. One is generated for each texture used in the brush. This has subkeys 'name' and 'var' - the entity name and shader variable to be modified. MergeBrushes must be enabled if this is present. * MakeLaserField generates a brush stretched across the whole area. * 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. """ model_name = res['modelname', None] make_unique = res.bool('UniqueModel') fizz_name = base_inst['targetname', ''] # search for the model instances model_targetnames = ( fizz_name + '_modelStart', fizz_name + '_modelEnd', ) is_laser = False for inst in vbsp.VMF.by_class['func_instance']: if inst['targetname'] in model_targetnames: if inst.fixup['skin', '0'] == '2': is_laser = True if model_name is not None: if model_name == '': inst['targetname'] = base_inst['targetname'] else: inst['targetname'] = ( base_inst['targetname'] + '-' + model_name ) if make_unique: inst.make_unique() for key, value in base_inst.fixup.items(): inst.fixup[key] = value base_inst.fixup['$is_laser'] = is_laser new_brush_config = list(res.find_all('brush')) if len(new_brush_config) == 0: return # No brush modifications if is_laser: # This is a laserfield! We can't edit those brushes! LOGGER.warning('CustFizzler executed on LaserField!') return # Record which materialmodify controls are used, so we can add if needed. # Conf id -> (brush_name, conf, [textures]) modify_controls = {} for orig_brush in ( vbsp.VMF.by_class['trigger_portal_cleanser'] & vbsp.VMF.by_target[fizz_name + '_brush']): orig_brush.remove() for config in new_brush_config: new_brush = orig_brush.copy() # Unique to the particular config property & fizzler name conf_key = (id(config), fizz_name) if config.bool('SimplifyBrush'): # Replace the brush with a simple one of the same size. bbox_min, bbox_max = new_brush.get_bbox() new_brush.solids = [vbsp.VMF.make_prism( bbox_min, bbox_max, mat=const.Tools.NODRAW, ).solid] should_merge = config.bool('MergeBrushes') if should_merge and conf_key in FIZZ_BRUSH_ENTS: # These are shared by both ents, but new_brush won't be added to # the map. (We need it though for the widening code to work). FIZZ_BRUSH_ENTS[conf_key].solids.extend(new_brush.solids) else: vbsp.VMF.add_ent(new_brush) # Don't allow restyling it vbsp.IGNORED_BRUSH_ENTS.add(new_brush) new_brush.clear_keys() # Wipe the original keyvalues new_brush['origin'] = orig_brush['origin'] new_brush['targetname'] = conditions.local_name( base_inst, config['name'], ) # All ents must have a classname! new_brush['classname'] = 'trigger_portal_cleanser' conditions.set_ent_keys( new_brush, base_inst, config, ) for out_prop in config.find_children('Outputs'): out = Output.parse(out_prop) out.comma_sep = False out.target = conditions.local_name( base_inst, out.target ) new_brush.add_out(out) if should_merge: # The first brush... FIZZ_BRUSH_ENTS[conf_key] = new_brush mat_mod_conf = config.find_key('MaterialModify', []) if mat_mod_conf: try: used_materials = modify_controls[id(mat_mod_conf)][2] except KeyError: used_materials = set() modify_controls[id(mat_mod_conf)] = ( new_brush['targetname'], mat_mod_conf, used_materials ) # It can only parent to one brush, so it can't attach # to them all properly. if not should_merge: raise Exception( "MaterialModify won't work without MergeBrushes!" ) else: used_materials = None laserfield_conf = config.find_key('MakeLaserField', None) if laserfield_conf.value is not None: # Resize the brush into a laserfield format, without # the 128*64 parts. If the brush is 128x128, we can # skip the resizing since it's already correct. laser_tex = laserfield_conf['texture', const.Special.LASERFIELD] nodraw_tex = laserfield_conf['nodraw', const.Tools.NODRAW] tex_width = laserfield_conf.int('texwidth', 512) is_short = False for side in new_brush.sides(): if side == const.Fizzler.SHORT: is_short = True break if is_short: for side in new_brush.sides(): if side == const.Fizzler.SHORT: side.mat = laser_tex side.uaxis.offset = 0 side.scale = 0.25 else: side.mat = nodraw_tex else: # The hard part - stretching the brush. convert_to_laserfield( new_brush, laser_tex, nodraw_tex, tex_width, ) if used_materials is not None: used_materials.add(laser_tex.casefold()) else: # Just change the textures for side in new_brush.sides(): try: tex_cat = TEX_FIZZLER[side.mat.casefold()] side.mat = config[tex_cat] except (KeyError, IndexError): # If we fail, just use the original textures pass else: if used_materials is not None and tex_cat != 'nodraw': used_materials.add(side.mat.casefold()) widen_amount = config.float('thickness', 2.0) if widen_amount != 2: for brush in new_brush.solids: conditions.widen_fizz_brush( brush, thickness=widen_amount, ) for brush_name, config, textures in modify_controls.values(): skip_if_static = config.bool('dynamicOnly', True) if skip_if_static and base_inst.fixup['$connectioncount'] == '0': continue mat_mod_name = config['name', 'modify'] var = config['var', '$outputintensity'] if not var.startswith('$'): var = '$' + var for tex in textures: vbsp.VMF.create_ent( classname='material_modify_control', origin=base_inst['origin'], targetname=conditions.local_name(base_inst, mat_mod_name), materialName='materials/' + tex + '.vmt', materialVar=var, parentname=brush_name, )
def res_conveyor_belt(inst: Entity, res: Property): """Create a conveyor belt. Options: SegmentInst: Generated at each square. ('track' is the name of the path.) TrackTeleport: Set the track points so they teleport trains to the start. Speed: The fixup or number for the train speed. MotionTrig: If set, a trigger_multiple will be spawned that EnableMotions weighted cubes. The value is the name of the relevant filter. EndOutput: Adds an output to the last track. The value is the same as outputs in VMFs. RotateSegments: If true (default), force segments to face in the direction of movement RailTemplate: A template for the railings. This is made into a non-solid func_brush, combining all sections. """ move_dist = srctools.conv_int(inst.fixup['$travel_distance']) if move_dist <= 2: # There isn't room for a catwalk, so don't bother. inst.remove() return move_dir = Vec(1, 0, 0).rotate_by_str(inst.fixup['$travel_direction']) move_dir.rotate_by_str(inst['angles']) start_offset = srctools.conv_float(inst.fixup['$starting_position'], 0) teleport_to_start = res.bool('TrackTeleport', True) segment_inst_file = res['SegmentInst', ''] rail_template = res['RailTemplate', None] vmf = inst.map if segment_inst_file: segment_inst_file = conditions.resolve_inst(segment_inst_file)[0] track_speed = res['speed', None] start_pos = Vec.from_str(inst['origin']) end_pos = start_pos + move_dist * move_dir if start_offset > 0: # If an oscillating platform, move to the closest side.. offset = start_offset * move_dir # The instance is placed this far along, so move back to the end. start_pos -= offset end_pos -= offset if start_offset > 0.5: # Swap the direction of movement.. start_pos, end_pos = end_pos, start_pos inst['origin'] = start_pos # Find the angle which generates an instance pointing in the direction # of movement, with the same normal. norm = Vec(z=1).rotate_by_str(inst['angles']) for roll in range(0, 360, 90): angles = move_dir.to_angle(roll) if Vec(z=1).rotate(*angles) == norm: break else: raise ValueError( "Can't find angles to give a" ' z={} and x={}!'.format(norm, move_dir) ) if res.bool('rotateSegments', True): inst['angles'] = angles else: angles = Vec.from_str(inst['angles']) # Add the EnableMotion trigger_multiple seen in platform items. # This wakes up cubes when it starts moving. motion_filter = res['motionTrig', None] # Disable on walls, or if the conveyor can't be turned on. if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0': motion_filter = None track_name = conditions.local_name(inst, 'segment_{}') rail_temp_solids = [] last_track = None # Place beams at the top, so they don't appear inside wall sections. beam_start = start_pos + 48 * norm # type: Vec beam_end = end_pos + 48 * norm # type: Vec for index, pos in enumerate(beam_start.iter_line(beam_end, stride=128), start=1): track = vmf.create_ent( classname='path_track', targetname=track_name.format(index) + '-track', origin=pos, spawnflags=0, orientationtype=0, # Don't rotate ) if track_speed is not None: track['speed'] = track_speed if last_track: last_track['target'] = track['targetname'] if index == 1 and teleport_to_start: track['spawnflags'] = 16 # Teleport here.. last_track = track # Don't place at the last point - it doesn't teleport correctly, # and would be one too many. if segment_inst_file and pos != end_pos: seg_inst = vmf.create_ent( classname='func_instance', targetname=track_name.format(index), file=segment_inst_file, origin=pos, angles=angles, ) seg_inst.fixup.update(inst.fixup) if rail_template: temp = conditions.import_template( rail_template, pos, angles, force_type=conditions.TEMP_TYPES.world, add_to_map=False, ) rail_temp_solids.extend(temp.world) if rail_temp_solids: vmf.create_ent( classname='func_brush', origin=beam_start, spawnflags=1, # Ignore +USE solidity=1, # Not solid vrad_brush_cast_shadows=1, drawinfastreflection=1, ).solids = rail_temp_solids if teleport_to_start: # Link back to the first track.. last_track['target'] = track_name.format(1) + '-track' # Generate an env_beam pointing from the start to the end of the track. beam_keys = res.find_key('BeamKeys', []) if beam_keys.value: beam = vmf.create_ent(classname='env_beam') # 3 offsets - x = distance from walls, y = side, z = height beam_off = beam_keys.vec('origin', 0, 63, 56) for prop in beam_keys: beam[prop.real_name] = prop.value # Localise the targetname so it can be triggered.. beam['LightningStart'] = beam['targetname'] = conditions.local_name( inst, beam['targetname', 'beam'] ) del beam['LightningEnd'] beam['origin'] = start_pos + Vec( -beam_off.x, beam_off.y, beam_off.z, ).rotate(*angles) beam['TargetPoint'] = end_pos + Vec( +beam_off.x, beam_off.y, beam_off.z, ).rotate(*angles) # Allow adding outputs to the last path_track. for prop in res.find_all('EndOutput'): output = Output.parse(prop) output.output = 'OnPass' output.inst_out = None output.comma_sep = False output.target = conditions.local_name(inst, output.target) last_track.add_out(output) if motion_filter is not None: motion_trig = vmf.create_ent( classname='trigger_multiple', targetname=conditions.local_name(inst, 'enable_motion_trig'), origin=start_pos, filtername=motion_filter, startDisabled=1, wait=0.1, ) motion_trig.add_out(Output('OnStartTouch', '!activator', 'ExitDisabledState')) # Match the size of the original... motion_trig.solids.append(vmf.make_prism( start_pos + Vec(72, -56, 58).rotate(*angles), end_pos + Vec(-72, 56, 144).rotate(*angles), mat='tools/toolstrigger', ).solid) if res.bool('NoPortalFloor'): # Block portals on the floor.. floor_noportal = vmf.create_ent( classname='func_noportal_volume', origin=beam_start, ) floor_noportal.solids.append(vmf.make_prism( start_pos + Vec(-60, -60, -66).rotate(*angles), end_pos + Vec(60, 60, -60).rotate(*angles), mat='tools/toolsinvisible', ).solid) # A brush covering under the platform. base_trig = vmf.make_prism( start_pos + Vec(-64, -64, 48).rotate(*angles), end_pos + Vec(64, 64, 56).rotate(*angles), mat='tools/toolsinvisible', ).solid vmf.add_brush(base_trig) # Make a paint_cleanser under the belt.. if res.bool('PaintFizzler'): pfizz = vmf.create_ent( classname='trigger_paint_cleanser', origin=start_pos, ) pfizz.solids.append(base_trig.copy()) for face in pfizz.sides(): face.mat = 'tools/toolstrigger'
def res_resizeable_trigger(vmf: VMF, res: Property): """Replace two markers with a trigger brush. This is run once to affect all of an item. Options: * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename. * `markerItem`: The item's ID * `previewConf`: A item config which enables/disables the preview overlay. * `previewInst`: An instance to place at the marker location in preview mode. This should contain checkmarks to display the value when testing. * `previewMat`: If set, the material to use for an overlay func_brush. The brush will be parented to the trigger, so it vanishes once killed. It is also non-solid. * `previewScale`: The scale for the func_brush materials. * `previewActivate`, `previewDeactivate`: The VMF output to turn the previewInst on and off. * `triggerActivate, triggerDeactivate`: The `instance:name;Output` outputs used when the trigger turns on or off. * `coopVar`: The instance variable which enables detecting both Coop players. The trigger will be a trigger_playerteam. * `coopActivate, coopDeactivate`: The `instance:name;Output` outputs used when coopVar is enabled. These should be suitable for a logic_coop_manager. * `coopOnce`: If true, kill the manager after it first activates. * `keys`: A block of keyvalues for the trigger brush. Origin and targetname will be set automatically. * `localkeys`: The same as above, except values will be changed to use instance-local names. """ marker = instanceLocs.resolve(res['markerInst']) marker_names = set() for inst in vmf.by_class['func_instance']: if inst['file'].casefold() in marker: marker_names.add(inst['targetname']) # Unconditionally delete from the map, so it doesn't # appear even if placed wrongly. inst.remove() if not marker_names: # No markers in the map - abort return RES_EXHAUSTED item_id = res['markerItem'] # Synthesise the item type used for the final trigger. item_type_sp = connections.ItemType( id=item_id + ':TRIGGER', output_act=Output.parse_name(res['triggerActivate', 'OnStartTouchAll']), output_deact=Output.parse_name(res['triggerDeactivate', 'OnEndTouchAll']), ) # For Coop, we add a logic_coop_manager in the mix so both players can # be handled. try: coop_var = res['coopVar'] except LookupError: coop_var = item_type_coop = None coop_only_once = False else: coop_only_once = res.bool('coopOnce') item_type_coop = connections.ItemType( id=item_id + ':TRIGGER_COOP', output_act=Output.parse_name(res['coopActivate', 'OnChangeToAllTrue']), output_deact=Output.parse_name(res['coopDeactivate', 'OnChangeToAnyFalse']), ) # Display preview overlays if it's preview mode, and the config is true pre_act = pre_deact = None if vbsp.IS_PREVIEW and vbsp_options.get_itemconf(res['previewConf', ''], False): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] preview_scale = res.float('previewScale', 0.25) # None if not found. with suppress(LookupError): pre_act = Output.parse(res.find_key('previewActivate')) with suppress(LookupError): pre_deact = Output.parse(res.find_key('previewDeactivate')) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' preview_scale = 0.25 # Now go through each brush. # We do while + pop to allow removing both names each loop through. todo_names = set(marker_names) while todo_names: targ = todo_names.pop() mark1 = connections.ITEMS.pop(targ) for conn in mark1.outputs: if conn.to_item.name in marker_names: mark2 = conn.to_item conn.remove() # Delete this connection. todo_names.discard(mark2.name) del connections.ITEMS[mark2.name] break else: if not mark1.inputs: # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. mark2 = mark1 else: # It's a marker with an input, the other in the pair # will handle everything. # But reinstate it in ITEMS. connections.ITEMS[targ] = mark1 continue inst1 = mark1.inst inst2 = mark2.inst is_coop = coop_var is not None and vbsp.GAME_MODE == 'COOP' and ( inst1.fixup.bool(coop_var) or inst2.fixup.bool(coop_var)) bbox_min, bbox_max = Vec.bbox(Vec.from_str(inst1['origin']), Vec.from_str(inst2['origin'])) origin = (bbox_max + bbox_min) / 2 # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vmf.create_ent( classname='trigger_multiple', # Default targetname=targ, origin=origin, angles='0 0 0', ) trig_ent.solids = [ vmf.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent = manager = vmf.create_ent( classname='logic_coop_manager', targetname=conditions.local_name(inst, 'man'), origin=origin, ) item = connections.Item( out_ent, item_type_coop, mark1.ant_floor_style, mark1.ant_wall_style, ) if coop_only_once: # Kill all the ents when both players are present. manager.add_out( Output('OnChangeToAllTrue', manager, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', manager, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', manager, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', manager, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', manager, 'SetStateBFalse'), ) else: item = connections.Item( trig_ent, item_type_sp, mark1.ant_floor_style, mark1.ant_wall_style, ) # Register, and copy over all the antlines. connections.ITEMS[item.name] = item item.ind_panels = mark1.ind_panels | mark2.ind_panels item.antlines = mark1.antlines | mark2.antlines item.shape_signs = mark1.shape_signs + mark2.shape_signs if preview_mat: preview_brush = vmf.create_ent( classname='func_brush', parentname=targ, origin=origin, Solidity='1', # Not solid drawinfastreflection='1', # Draw in goo.. # Disable shadows and lighting.. disableflashlight='1', disablereceiveshadows='1', disableshadowdepth='1', disableshadows='1', ) preview_brush.solids = [ # Make it slightly smaller, so it doesn't z-fight with surfaces. vmf.make_prism( bbox_min + 0.5, bbox_max - 0.5, mat=preview_mat, ).solid, ] for face in preview_brush.sides(): face.scale = preview_scale if preview_inst_file: pre_inst = vmf.create_ent( classname='func_instance', targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=inst2['origin'], ) if pre_act is not None: out = pre_act.copy() out.inst_out, out.output = item.output_act() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) if pre_deact is not None: out = pre_deact.copy() out.inst_out, out.output = item.output_deact() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) for conn in mark1.outputs | mark2.outputs: conn.from_item = item return RES_EXHAUSTED
def parse(item_id: str, conf: Property): """Read the item type info from the given config.""" def get_outputs(prop_name): """Parse all the outputs with this name.""" return [ Output.parse(prop) for prop in conf.find_all(prop_name) # Allow blank to indicate no output. if prop.value != '' ] enable_cmd = get_outputs('enable_cmd') disable_cmd = get_outputs('disable_cmd') lock_cmd = get_outputs('lock_cmd') unlock_cmd = get_outputs('unlock_cmd') inf_lock_only = conf.bool('inf_lock_only') timer_done_cmd = get_outputs('timer_done_cmd') if 'timer_sound_pos' in conf: timer_sound_pos = conf.vec('timer_sound_pos') force_timer_sound = conf.bool('force_timer_sound') else: timer_sound_pos = None force_timer_sound = False try: input_type = InputType( conf['Type', 'default'].casefold() ) except ValueError: raise ValueError('Invalid input type "{}": {}'.format( item_id, conf['type'], )) from None invert_var = conf['invertVar', '0'] try: spawn_fire = FeatureMode(conf['spawnfire', 'never'].casefold()) except ValueError: # Older config option - it was a bool for always/never. spawn_fire_bool = conf.bool('spawnfire', None) if spawn_fire_bool is None: raise # Nope, not a bool. spawn_fire = FeatureMode.ALWAYS if spawn_fire_bool else FeatureMode.NEVER try: sec_spawn_fire = FeatureMode(conf['sec_spawnfire', 'never'].casefold()) except ValueError: # Default to primary value. sec_spawn_fire = FeatureMode.NEVER if input_type is InputType.DUAL: sec_enable_cmd = get_outputs('sec_enable_cmd') sec_disable_cmd = get_outputs('sec_disable_cmd') try: default_dual = CONN_TYPE_NAMES[ conf['Default_Dual', 'primary'].casefold() ] except KeyError: raise ValueError('Invalid default type for "{}": {}'.format( item_id, conf['Default_Dual'], )) from None # We need an affinity to use when nothing else specifies it. if default_dual is ConnType.DEFAULT: raise ValueError('Must specify a default type for "{}"!'.format( item_id, )) from None sec_invert_var = conf['sec_invertVar', '0'] else: # No dual type, set to dummy values. sec_enable_cmd = [] sec_disable_cmd = [] default_dual = ConnType.DEFAULT sec_invert_var = '' try: output_type = CONN_TYPE_NAMES[ conf['DualType', 'default'].casefold() ] except KeyError: raise ValueError('Invalid output affinity for "{}": {}'.format( item_id, conf['DualType'], )) from None def get_input(prop_name: str): """Parse an input command.""" try: return Output.parse_name(conf[prop_name]) except IndexError: return None out_act = get_input('out_activate') out_deact = get_input('out_deactivate') out_lock = get_input('out_lock') out_unlock = get_input('out_unlock') timer_start = timer_stop = None if 'out_timer_start' in conf: timer_start = [ Output.parse_name(prop.value) for prop in conf.find_all('out_timer_start') if prop.value ] if 'out_timer_stop' in conf: timer_stop = [ Output.parse_name(prop.value) for prop in conf.find_all('out_timer_stop') if prop.value ] return Config( item_id, default_dual, input_type, spawn_fire, invert_var, enable_cmd, disable_cmd, sec_spawn_fire, sec_invert_var, sec_enable_cmd, sec_disable_cmd, output_type, out_act, out_deact, lock_cmd, unlock_cmd, out_lock, out_unlock, inf_lock_only, timer_sound_pos, timer_done_cmd, force_timer_sound, timer_start, timer_stop, )
def res_fix_rotation_axis(vmf: VMF, ent: Entity, res: Property): """Properly setup rotating brush entities to match the instance. This uses the orientation of the instance to determine the correct spawnflags to make it rotate in the correct direction. This can either modify an existing entity (which may be in an instance), or generate a new one. The generated brush will be 2x2x2 units large, and always set to be non-solid. For both modes: - `Axis`: specifies the rotation axis local to the instance. - `Reversed`: If set, flips the direction around. - `Classname`: Specifies which entity, since the spawnflags required varies. For application to an existing entity: - `ModifyTarget`: The local name of the entity to modify. For brush generation mode: - `Pos` and `name` are local to the instance, and will set the `origin` and `targetname` respectively. - `Keys` are any other keyvalues to be be set. - `Flags` sets additional spawnflags. Multiple values may be separated by `+`, and will be added together. - `Classname` specifies which entity will be created, as well as which other values will be set to specify the correct orientation. - `AddOut` is used to add outputs to the generated entity. It takes the options `Output`, `Target`, `Input`, `Inst_targ`, `Param` and `Delay`. If `Inst_targ` is defined, it will be used with the input to construct an instance proxy input. If `OnceOnly` is set, the output will be deleted when fired. Permitted entities: * [`func_door_rotating`](https://developer.valvesoftware.com/wiki/func_door_rotating) * [`func_platrot`](https://developer.valvesoftware.com/wiki/func_platrot) * [`func_rot_button`](https://developer.valvesoftware.com/wiki/func_rot_button) * [`func_rotating`](https://developer.valvesoftware.com/wiki/func_rotating) * [`momentary_rot_button`](https://developer.valvesoftware.com/wiki/momentary_rot_button) """ des_axis = res['axis', 'z'].casefold() reverse = res.bool('reversed') door_type = res['classname', 'func_door_rotating'] orient = Matrix.from_angle(Angle.from_str(ent['angles'])) axis = round(Vec.with_axes(des_axis, 1) @ orient, 6) if axis.x > 0 or axis.y > 0 or axis.z > 0: # If it points forward, we need to reverse the rotating door reverse = not reverse axis = abs(axis) try: flag_values = FLAG_ROTATING[door_type] except KeyError: LOGGER.warning('Unknown rotating brush type "{}"!', door_type) return name = res['ModifyTarget', ''] door_ent: Entity | None if name: name = conditions.local_name(ent, name) setter_loc = ent['origin'] door_ent = None spawnflags = 0 else: # Generate a brush. name = conditions.local_name(ent, res['name', '']) pos = res.vec('Pos') @ Angle.from_str(ent['angles', '0 0 0']) pos += Vec.from_str(ent['origin', '0 0 0']) setter_loc = str(pos) door_ent = vmf.create_ent( classname=door_type, targetname=name, origin=pos.join(' '), ) # Extra stuff to apply to the flags (USE, toggle, etc) spawnflags = sum( map( # Add together multiple values srctools.conv_int, res['flags', '0'].split('+') # Make the door always non-solid! )) | flag_values.get('solid_flags', 0) conditions.set_ent_keys(door_ent, ent, res) for output in res.find_all('AddOut'): door_ent.add_out( Output( out=output['Output', 'OnUse'], inp=output['Input', 'Use'], targ=output['Target', ''], inst_in=output['Inst_targ', None], param=output['Param', ''], delay=srctools.conv_float(output['Delay', '']), times=(1 if srctools.conv_bool(output['OnceOnly', False]) else -1), )) # Generate brush door_ent.solids = [vmf.make_prism(pos - 1, pos + 1).solid] # Add or remove flags as needed for flag, value in zip( ('x', 'y', 'z', 'rev'), [axis.x > 1e-6, axis.y > 1e-6, axis.z > 1e-6, reverse], ): if flag not in flag_values: continue if door_ent is not None: if value: spawnflags |= flag_values[flag] else: spawnflags &= ~flag_values[flag] else: # Place a KV setter to set this. vmf.create_ent( 'comp_kv_setter', origin=setter_loc, target=name, mode='flags', kv_name=flag_values[flag], kv_value_global=value, ) if door_ent is not None: door_ent['spawnflags'] = spawnflags # This ent uses a keyvalue for reversing... if door_type == 'momentary_rot_button': vmf.create_ent( 'comp_kv_setter', origin=setter_loc, target=name, mode='kv', kv_name='StartDirection', kv_value_global='1' if reverse else '-1', )
def res_import_template(vmf: VMF, coll: Collisions, res: Property): """Import a template VMF file, retexturing it to match orientation. It will be placed overlapping the given instance. If no block is used, only ID can be specified. Options: - `ID`: The ID of the template to be inserted. Add visgroups to additionally add after a colon, comma-seperated (`temp_id:vis1,vis2`). Either section, or the whole value can be a `$fixup`. - `angles`: Override the instance rotation, so it is always rotated this much. - `rotation`: Apply the specified rotation before the instance's rotation. - `offset`: Offset the template from the instance's position. - `force`: a space-seperated list of overrides. If 'white' or 'black' is present, the colour of tiles will be overridden. If `invert` is added, white/black tiles will be swapped. If a tile size (`2x2`, `4x4`, `wall`, `special`) is included, all tiles will be switched to that size (if not a floor/ceiling). If 'world' or 'detail' is present, the brush will be forced to that type. - `replace`: A block of template material -> replacement textures. This is case insensitive - any texture here will not be altered otherwise. If the material starts with a `#`, it is instead a list of face IDs separated by spaces. If the result evaluates to "", no change occurs. Both can be $fixups (parsed first). - `bindOverlay`: Bind overlays in this template to the given surface, and bind overlays on a surface to surfaces in this template. The value specifies the offset to the surface, where 0 0 0 is the floor position. It can also be a block of multiple positions. - `alignBindOverlay`: If set, align the bindOverlay offsets to the grid. - `keys`/`localkeys`: If set, a brush entity will instead be generated with these values. This overrides force world/detail. Specially-handled keys: - `"origin"`, offset automatically. - `"movedir"` on func_movelinear - set a normal surrounded by `<>`, this gets replaced with angles. - `colorVar`: If this fixup var is set to `white` or `black`, that colour will be forced. If the value is `<editor>`, the colour will be chosen based on the color of the surface for ItemButtonFloor, funnels or entry/exit frames. - `invertVar`: If this fixup value is true, tile colour will be swapped to the opposite of the current force option. This applies after colorVar. - `visgroup`: Sets how visgrouped parts are handled. Several values are possible: - A property block: Each name should match a visgroup, and the value should be a block of flags that if true enables that group. - 'none' (default): All extra groups are ignored. - 'choose': One group is chosen randomly. - a number: The percentage chance for each visgroup to be added. - `visgroup_force_var`: If set and True, visgroup is ignored and all groups are added. - `pickerVars`: If this is set, the results of colorpickers can be read out of the template. The key is the name of the picker, the value is the fixup name to write to. The output is either 'white', 'black' or ''. - `outputs`: Add outputs to the brush ent. Syntax is like VMFs, and all names are local to the instance. - `senseOffset`: If set, colorpickers and tilesetters will be treated as being offset by this amount. """ if res.has_children(): orig_temp_id = res['id'] else: orig_temp_id = res.value res = Property('TemplateBrush', []) force = res['force', ''].casefold().split() if 'white' in force: conf_force_colour = texturing.Portalable.white elif 'black' in force: conf_force_colour = texturing.Portalable.black elif 'invert' in force: conf_force_colour = 'INVERT' else: conf_force_colour = None if 'world' in force: force_type = template_brush.TEMP_TYPES.world elif 'detail' in force: force_type = template_brush.TEMP_TYPES.detail else: force_type = template_brush.TEMP_TYPES.default force_grid: texturing.TileSize | None size: texturing.TileSize for size in texturing.TileSize: if size in force: force_grid = size break else: force_grid = None if 'bullseye' in force: surf_cat = texturing.GenCat.BULLSEYE elif 'special' in force or 'panel' in force: surf_cat = texturing.GenCat.PANEL else: surf_cat = texturing.GenCat.NORMAL replace_tex: dict[str, list[str]] = {} for prop in res.find_block('replace', or_blank=True): replace_tex.setdefault(prop.name, []).append(prop.value) if 'replaceBrush' in res: LOGGER.warning( 'replaceBrush command used for template "{}", which is no ' 'longer used.', orig_temp_id, ) bind_tile_pos = [ # So it's the floor block location. Vec.from_str(value) - (0, 0, 128) for value in res.find_key('BindOverlay', or_blank=True).as_array() ] align_bind_overlay = res.bool('alignBindOverlay') key_values = res.find_block("Keys", or_blank=True) if key_values: key_block = Property("", [ key_values, res.find_block("LocalKeys", or_blank=True), ]) # Ensure we have a 'origin' keyvalue - we automatically offset that. if 'origin' not in key_values: key_values['origin'] = '0 0 0' # Spawn everything as detail, so they get put into a brush # entity. force_type = template_brush.TEMP_TYPES.detail outputs = [Output.parse(prop) for prop in res.find_children('Outputs')] else: key_block = None outputs = [] # None = don't add any more. visgroup_func: Callable[[Random, list[str]], Iterable[str]] | None = None try: # allow both spellings. visgroup_prop = res.find_key('visgroups') except NoKeyError: visgroup_prop = res.find_key('visgroup', 'none') if visgroup_prop.has_children(): visgroup_instvars = list(visgroup_prop) else: visgroup_instvars = [] visgroup_mode = res['visgroup', 'none'].casefold() # Generate the function which picks which visgroups to add to the map. if visgroup_mode == 'none': pass elif visgroup_mode == 'choose': def visgroup_func(rng: Random, groups: list[str]) -> Iterable[str]: """choose = add one random group.""" return [rng.choice(groups)] else: percent = srctools.conv_float(visgroup_mode.rstrip('%'), 0.00) if percent > 0.0: def visgroup_func(rng: Random, groups: list[str]) -> Iterable[str]: """Number = percent chance for each to be added""" for group in sorted(groups): if rng.uniform(0, 100) <= percent: yield group picker_vars = [(prop.real_name, prop.value) for prop in res.find_children('pickerVars')] try: ang_override = to_matrix(Angle.from_str(res['angles'])) except LookupError: ang_override = None try: rotation = to_matrix(Angle.from_str(res['rotation'])) except LookupError: rotation = Matrix() offset = res['offset', '0 0 0'] invert_var = res['invertVar', ''] color_var = res['colorVar', ''] if color_var.casefold() == '<editor>': color_var = '<editor>' # If true, force visgroups to all be used. visgroup_force_var = res['forceVisVar', ''] sense_offset = res.vec('senseOffset') def place_template(inst: Entity) -> None: """Place a template.""" temp_id = inst.fixup.substitute(orig_temp_id) # Special case - if blank, just do nothing silently. if not temp_id: return temp_name, visgroups = template_brush.parse_temp_name(temp_id) try: template = template_brush.get_template(temp_name) except template_brush.InvalidTemplateName: # If we did lookup, display both forms. if temp_id != orig_temp_id: LOGGER.warning('{} -> "{}" is not a valid template!', orig_temp_id, temp_name) else: LOGGER.warning('"{}" is not a valid template!', temp_name) # We don't want an error, just quit. return for vis_flag_block in visgroup_instvars: if all( conditions.check_flag(flag, coll, inst) for flag in vis_flag_block): visgroups.add(vis_flag_block.real_name) force_colour = conf_force_colour if color_var == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = texturing.Portalable.white elif 'black' in traits: force_colour = texturing.Portalable.black else: LOGGER.warning( '"{}": Instance "{}" ' "isn't one with inherent color!", temp_id, inst['file'], ) elif color_var: color_val = conditions.resolve_value(inst, color_var).casefold() if color_val == 'white': force_colour = texturing.Portalable.white elif color_val == 'black': force_colour = texturing.Portalable.black # else: no color var if srctools.conv_bool(conditions.resolve_value(inst, invert_var)): force_colour = template_brush.TEMP_COLOUR_INVERT[conf_force_colour] # else: False value, no invert. if ang_override is not None: orient = ang_override else: orient = rotation @ Angle.from_str(inst['angles', '0 0 0']) origin = conditions.resolve_offset(inst, offset) # If this var is set, it forces all to be included. if srctools.conv_bool( conditions.resolve_value(inst, visgroup_force_var)): visgroups.update(template.visgroups) elif visgroup_func is not None: visgroups.update( visgroup_func( rand.seed(b'temp', template.id, origin, orient), list(template.visgroups), )) LOGGER.debug('Placing template "{}" at {} with visgroups {}', template.id, origin, visgroups) temp_data = template_brush.import_template( vmf, template, origin, orient, targetname=inst['targetname'], force_type=force_type, add_to_map=True, coll=coll, additional_visgroups=visgroups, bind_tile_pos=bind_tile_pos, align_bind=align_bind_overlay, ) if key_block is not None: conditions.set_ent_keys(temp_data.detail, inst, key_block) br_origin = Vec.from_str(key_block.find_key('keys')['origin']) br_origin.localise(origin, orient) temp_data.detail['origin'] = br_origin move_dir = temp_data.detail['movedir', ''] if move_dir.startswith('<') and move_dir.endswith('>'): move_dir = Vec.from_str(move_dir) @ orient temp_data.detail['movedir'] = move_dir.to_angle() for out in outputs: out = out.copy() out.target = conditions.local_name(inst, out.target) temp_data.detail.add_out(out) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, surf_cat, sense_offset, ) for picker_name, picker_var in picker_vars: picker_val = temp_data.picker_results.get(picker_name, None) if picker_val is not None: inst.fixup[picker_var] = picker_val.value else: inst.fixup[picker_var] = '' return place_template
def res_add_brush(vmf: VMF, inst: Entity, res: Property) -> None: """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 `1/0`. 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. """ origin = Vec.from_str(inst['origin']) angles = Angle.from_str(inst['angles']) 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 # Rotate to match the instance point1 = point1 @ angles + origin point2 = point2 @ angles + origin try: tex_type = texturing.Portalable(res['type', 'black']) except ValueError: LOGGER.warning( 'AddBrush: "{}" is not a valid brush ' 'color! (white or black)', res['type'], ) tex_type = texturing.Portalable.BLACK dim = round(point2 - point1, 6) 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. tile_grids = { 'x': tiling.TileSize.TILE_4x4, 'y': tiling.TileSize.TILE_4x4, 'z': tiling.TileSize.TILE_4x4, } for axis in 'xyz': u, v = Vec.INV_AXIS[axis] max_size = min(dim[u], dim[v]) if max_size % 128 == 0: tile_grids[axis] = tiling.TileSize.TILE_1x1 elif dim[u] % 64 == 0 and dim[v] % 128 == 0: tile_grids[axis] = tiling.TileSize.TILE_2x1 elif max_size % 64 == 0: tile_grids[axis] = tiling.TileSize.TILE_2x2 else: tile_grids[axis] = tiling.TileSize.TILE_4x4 solids = vmf.make_prism(point1, point2) solids.north.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.N), tex_type, ).get(solids.north.get_origin(), tile_grids['y']) solids.south.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.S), tex_type, ).get(solids.north.get_origin(), tile_grids['y']) solids.east.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.E), tex_type, ).get(solids.north.get_origin(), tile_grids['x']) solids.west.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.W), tex_type, ).get(solids.north.get_origin(), tile_grids['x']) solids.top.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.T), tex_type, ).get(solids.north.get_origin(), tile_grids['z']) solids.bottom.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.B), tex_type, ).get(solids.north.get_origin(), tile_grids['z']) if res.bool('detail'): # 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)
def res_add_overlay_inst(inst: Entity, res: Property): """Add another instance on top of this one. Values: File: The filename. Fixup Style: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. Copy_Fixup: If true, all the $replace values from the original instance will be copied over. move_outputs: If true, outputs will be moved to this instance. offset: The offset (relative to the base) that the instance will be placed. Can be set to '<piston_top>' and '<piston_bottom>' to offset based on the configuration. '<piston_start>' will set it to the starting position, and '<piston_end>' will set it to the ending position. of piston platform handles. angles: If set, overrides the base instance angles. This does not affect the offset property. fixup/localfixup: Keyvalues in this block will be copied to the overlay entity. If the value starts with $, the variable will be copied over. If this is present, copy_fixup will be disabled. """ angle = res["angles", inst["angles", "0 0 0"]] overlay_inst = vbsp.VMF.create_ent( classname="func_instance", targetname=inst["targetname", ""], file=resolve_inst(res["file", ""])[0], angles=angle, origin=inst["origin"], fixup_style=res["fixup_style", "0"], ) # Don't run if the fixup block exists.. if srctools.conv_bool(res["copy_fixup", "1"]): if "fixup" not in res and "localfixup" not in res: # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value conditions.set_ent_keys(overlay_inst.fixup, inst, res, "fixup") if res.bool("move_outputs", False): overlay_inst.outputs = inst.outputs inst.outputs = [] if "offset" in res: folded_off = res["offset"].casefold() # Offset the overlay by the given distance # Some special placeholder values: if folded_off == "<piston_start>": if srctools.conv_bool(inst.fixup["$start_up", ""]): folded_off = "<piston_top>" else: folded_off = "<piston_bottom>" elif folded_off == "<piston_end>": if srctools.conv_bool(inst.fixup["$start_up", ""]): folded_off = "<piston_bottom>" else: folded_off = "<piston_top>" if folded_off == "<piston_bottom>": offset = Vec(z=srctools.conv_int(inst.fixup["$bottom_level"]) * 128) elif folded_off == "<piston_top>": offset = Vec(z=srctools.conv_int(inst.fixup["$top_level"], 1) * 128) else: # Regular vector offset = Vec.from_str(conditions.resolve_value(inst, res["offset"])) offset.rotate_by_str(inst["angles", "0 0 0"]) overlay_inst["origin"] = (offset + Vec.from_str(inst["origin"])).join(" ") return overlay_inst
def res_resizeable_trigger(res: Property): """Replace two markers with a trigger brush. This is run once to affect all of an item. Options: 'markerInst': <ITEM_ID:1,2> value referencing the marker instances, or a filename. 'markerItem': The item's ID 'previewVar': A stylevar which enables/disables the preview overlay. 'previewinst': An instance to place at the marker location in preview mode. This should contain checkmarks to display the value when testing. 'previewMat': If set, the material to use for an overlay func_brush. The brush will be parented to the trigger, so it vanishes once killed. It is also non-solid. 'previewScale': The scale for the func_brush materials. 'previewActivate', 'previewDeactivate': The 'instance:name;Input' value to turn the previewInst on and off. 'triggerActivate, triggerDeactivate': The outputs used when the trigger turns on or off. 'coopVar': The instance variable which enables detecting both Coop players. The trigger will be a trigger_playerteam. 'coopActivate, coopDeactivate': The outputs used when coopVar is enabled. These should be suitable for a logic_coop_manager. 'coopOnce': If true, kill the manager after it first activates. 'keys': A block of keyvalues for the trigger brush. Origin and targetname will be set automatically. 'localkeys': The same as above, except values will be changed to use instance-local names. """ marker = resolve_inst(res['markerInst']) markers = {} for inst in vbsp.VMF.by_class['func_instance']: if inst['file'].casefold() in marker: markers[inst['targetname']] = inst if not markers: # No markers in the map - abort return RES_EXHAUSTED trig_act = res['triggerActivate', 'OnStartTouchAll'] trig_deact = res['triggerDeactivate','OnEndTouchAll'] coop_var = res['coopVar', None] coop_act = res['coopActivate', 'OnChangeToAllTrue'] coop_deact = res['coopDeactivate', 'OnChangeToAnyFalse'] coop_only_once = res.bool('coopOnce') marker_connection = conditions.CONNECTIONS[res['markerItem'].casefold()] mark_act_name, mark_act_out = marker_connection.out_act mark_deact_name, mark_deact_out = marker_connection.out_deact del marker_connection preview_var = res['previewVar', ''].casefold() # Display preview overlays if it's preview mode, and the style var is true # or does not exist if vbsp.IS_PREVIEW and (not preview_var or vbsp.settings['style_vars'][preview_var]): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] pre_act_name, pre_act_inp = Output.parse_name( res['previewActivate', '']) pre_deact_name, pre_deact_inp = Output.parse_name( res['previewDeactivate', '']) preview_scale = srctools.conv_float(res['previewScale', '0.25'], 0.25) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' pre_act_name = pre_deact_name = None pre_act_inp = pre_deact_inp = '' preview_scale = 0.25 # Now convert each brush # Use list() to freeze it, allowing us to delete from the dict for targ, inst in list(markers.items()): # type: str, VLib.Entity for out in inst.output_targets(): if out in markers: other = markers[out] # type: Entity del markers[out] # Don't let it get repeated break else: if inst.fixup['$connectioncount'] == '0': # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. other = inst else: continue # It's a marker with an input, the other in the pair # will handle everything. for ent in {inst, other}: # Only do once if inst == other ent.remove() is_coop = vbsp.GAME_MODE == 'COOP' and ( inst.fixup.bool(coop_var) or other.fixup.bool(coop_var) ) bbox_min, bbox_max = Vec.bbox( Vec.from_str(inst['origin']), Vec.from_str(other['origin']) ) # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vbsp.VMF.create_ent( classname='trigger_multiple', # Default # Use the 1st instance's name - that way other inputs control the # trigger itself. targetname=targ, origin=inst['origin'], angles='0 0 0', ) trig_ent.solids = [ vbsp.VMF.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent_name = conditions.local_name(inst, 'man') out_ent = vbsp.VMF.create_ent( classname='logic_coop_manager', targetname=out_ent_name, origin=inst['origin'] ) if coop_only_once: # Kill all the ents when both players are present. out_ent.add_out( Output('OnChangeToAllTrue', out_ent_name, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', out_ent_name, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', out_ent_name, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', out_ent_name, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', out_ent_name, 'SetStateBFalse'), ) act_out = coop_act deact_out = coop_deact else: act_out = trig_act deact_out = trig_deact if preview_mat: preview_brush = vbsp.VMF.create_ent( classname='func_brush', parentname=targ, origin=inst['origin'], Solidity='1', # Not solid drawinfastreflection='1', # Draw in goo.. # Disable shadows and lighting.. disableflashlight='1', disablereceiveshadows='1', disableshadowdepth='1', disableshadows='1', ) preview_brush.solids = [ # Make it slightly smaller, so it doesn't z-fight with surfaces. vbsp.VMF.make_prism( bbox_min + 0.5, bbox_max - 0.5, mat=preview_mat, ).solid, ] for face in preview_brush.sides(): face.scale = preview_scale if preview_inst_file: vbsp.VMF.create_ent( classname='func_instance', targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=other['origin'], ) if pre_act_name and trig_act: out_ent.add_out(Output( trig_act, targ + '_preview', inst_in=pre_act_name, inp=pre_act_inp, )) if pre_deact_name and trig_deact: out_ent.add_out(Output( trig_deact, targ + '_preview', inst_in=pre_deact_name, inp=pre_deact_inp, )) # Now copy over the outputs from the markers, making it work. for out in inst.outputs + other.outputs: # Skip the output joining the two markers together. if out.target == other['targetname']: continue if out.inst_out == mark_act_name and out.output == mark_act_out: ent_out = act_out elif out.inst_out == mark_deact_name and out.output == mark_deact_out: ent_out = deact_out else: continue # Skip this output - it's somehow invalid for this item. if not ent_out: continue # Allow setting the output to "" to skip out_ent.add_out(Output( ent_out, out.target, inst_in=out.inst_in, inp=out.input, param=out.params, delay=out.delay, times=out.times, )) return RES_EXHAUSTED
def generate_music_script(data: Property, pack_list: PackList) -> bytes: """Generate a soundscript file for music.""" # We also pack the filenames used for the tracks - that way funnel etc # only get packed when needed. Stock sounds are in VPKS or in aperturetag/, # we don't check there. # The voice attrs used in the map - we can skip tracks voice_attr = CONF['VoiceAttr', ''].casefold().split(';') funnel = data.find_key('tbeam', '') bounce = data.find_key('bouncegel', '') speed = data.find_key('speedgel', '') sync_funnel = data.bool('sync_funnel') if 'base' not in data: base = Property('base', 'bee2/silent_lp.wav') # Don't sync to a 2-second sound. sync_funnel = False else: base = data.find_key('base') # The sounds must be present, and the items should be in the map. has_funnel = funnel.value and ( 'funnel' in voice_attr or 'excursionfunnel' in voice_attr ) has_bounce = bounce.value and ( 'bouncegel' in voice_attr or 'bluegel' in voice_attr ) # Speed-gel sounds also play when flinging, so keep it always. file = StringIO() # Write the base music track file.write(MUSIC_START.format(name='', vol='1')) write_sound(file, base, pack_list, snd_prefix='#*') file.write(MUSIC_BASE) # The 'soundoperators' section is still open now. # Add the operators to play the auxilluary sounds.. if has_funnel: file.write(MUSIC_FUNNEL_MAIN) if has_bounce: file.write(MUSIC_GEL_BOUNCE_MAIN) if speed.value: file.write(MUSIC_GEL_SPEED_MAIN) # End the main sound block file.write(MUSIC_END) if has_funnel: # Write the 'music.BEE2_funnel' sound entry file.write('\n') file.write(MUSIC_START.format(name='_funnel', vol='1')) write_sound(file, funnel, pack_list, snd_prefix='*') # Some tracks want the funnel music to sync with the normal # track, others randomly choose a start. file.write( MUSIC_FUNNEL_SYNC_STACK if sync_funnel else MUSIC_FUNNEL_RAND_STACK ) file.write(MUSIC_FUNNEL_UPDATE_STACK) if has_bounce: file.write('\n') file.write(MUSIC_START.format(name='_gel_bounce', vol='0.5')) write_sound(file, bounce, pack_list, snd_prefix='*') # Fade in fast (we never get false positives, but fade out slow # since this disables when falling back.. file.write(MUSIC_GEL_STACK.format(fadein=0.25, fadeout=1.5)) if speed.value: file.write('\n') file.write(MUSIC_START.format(name='_gel_speed', vol='0.5')) write_sound(file, speed, pack_list, snd_prefix='*') # We need to shut off the sound fast, so portals don't confuse it. # Fade in slow so it doesn't make much sound (and also as we get # up to speed). We stop almost immediately on gel too. file.write(MUSIC_GEL_STACK.format(fadein=0.5, fadeout=0.1)) return file.getvalue().encode()
def res_set_tile(inst: Entity, res: Property) -> None: """Set 4x4 parts of a tile to the given values. `Offset` defines the position of the upper-left tile in the grid. Each `Tile` section defines a row of the positions to edit like so: "Tile" "bbbb" "Tile" "b..b" "Tile" "b..b" "Tile" "bbbb" If `Force` is true, the specified tiles will override any existing ones and create the tile if necessary. Otherwise they will be merged in - white/black tiles will not replace tiles set to nodraw or void for example. `chance`, if specified allows producing irregular tiles by randomly not changing the tile. If you need less regular placement (other orientation, precise positions) use a bee2_template_tilesetter in a template. Allowed tile characters: - `W`: White tile. - `w`: White 4x4 only tile. - `B`: Black tile. - `b`: Black 4x4 only tile. - `g`: The side/bottom of goo pits. - `n`: Nodraw surface. - `i`: Invert the tile surface, if black/white. - `1`: Convert to a 1x1 only tile, if a black/white tile. - `4`: Convert to a 4x4 only tile, if a black/white tile. - `.`: Void (remove the tile in this position). - `_` or ` `: Placeholder (don't modify this space). - `x`: Cutout Tile (Broken) - `o`: Cutout Tile (Partial) """ origin = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) offset = (res.vec('offset', -48, 48) - (0, 0, 64)) @ orient + origin norm = round(orient.up(), 6) force_tile = res.bool('force') tiles: list[str] = [ row.value for row in res if row.name in ('tile', 'tiles') ] if not tiles: raise ValueError('No "tile" parameters in SetTile!') chance = srctools.conv_float(res['chance', '100'].rstrip('%'), 100.0) if chance < 100.0: rng = rand.seed(b'tile', inst, res['seed', '']) else: rng = None for y, row in enumerate(tiles): for x, val in enumerate(row): if val in '_ ': continue if rng is not None and rng.uniform(0, 100) > chance: continue pos = Vec(32 * x, -32 * y, 0) @ orient + offset if val == '4': size = tiling.TileSize.TILE_4x4 elif val == '1': size = tiling.TileSize.TILE_1x1 elif val == 'i': size = None else: try: new_tile = tiling.TILETYPE_FROM_CHAR[val] except KeyError: LOGGER.warning('Unknown tiletype "{}"!', val) else: tiling.edit_quarter_tile(pos, norm, new_tile, force_tile) continue # Edit the existing tile. try: tile, u, v = tiling.find_tile(pos, norm, force_tile) except KeyError: LOGGER.warning( 'Expected tile, but none found: {}, {}', pos, norm, ) continue if size is None: # Invert the tile. tile[u, v] = tile[u, v].inverted continue # Unless forcing is enabled don't alter the size of GOO_SIDE. if tile[u, v].is_tile and tile[u, v] is not tiling.TileType.GOO_SIDE: tile[u, v] = tiling.TileType.with_color_and_size( size, tile[u, v].color) elif force_tile: # If forcing, make it black. Otherwise no need to change. tile[u, v] = tiling.TileType.with_color_and_size( size, tiling.Portalable.BLACK)
def brush_at_loc( inst: Entity, props: Property, ) -> Tuple[tiling.TileType, bool, Set[tiling.TileType]]: """Common code for posIsSolid and ReadSurfType. This returns the average tiletype, if both colors were found, and a set of all types found. """ origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) # Allow using pos1 instead, to match pos2. pos = props.vec('pos1' if 'pos1' in props else 'pos') pos.z -= 64 # Subtract so origin is the floor-position pos.localise(origin, angles) norm = props.vec('dir', 0, 0, 1).rotate(*angles) if props.bool('gridpos') and norm is not None: for axis in 'xyz': # Don't realign things in the normal's axis - # those are already fine. if norm[axis] == 0: pos[axis] = pos[axis] // 128 * 128 + 64 result_var = props['setVar', ''] # RemoveBrush is the pre-tiling name. should_remove = props.bool('RemoveTile', props.bool('RemoveBrush', False)) tile_types: Set[tiling.TileType] = set() both_colors = False if 'pos2' in props: pos2 = props.vec('pos2') pos2.z -= 64 # Subtract so origin is the floor-position pos2.localise(origin, angles) bbox_min, bbox_max = Vec.bbox(pos, pos2) white_count = black_count = 0 for pos in Vec.iter_grid(bbox_min, bbox_max, 32): try: tiledef, u, v = tiling.find_tile(pos, norm) except KeyError: continue tile_type = tiledef[u, v] tile_types.add(tile_type) if should_remove: tiledef[u, v] = tiling.TileType.VOID if tile_type.is_tile: if tile_type.color is tiling.Portalable.WHITE: white_count += 1 else: black_count += 1 both_colors = white_count > 0 and black_count > 0 if white_count == black_count == 0: tile_type = tiling.TileType.VOID tile_types.add(tiling.TileType.VOID) elif white_count > black_count: tile_type = tiling.TileType.WHITE else: tile_type = tiling.TileType.BLACK else: # Single tile. try: tiledef, u, v = tiling.find_tile(pos, norm) except KeyError: tile_type = tiling.TileType.VOID else: tile_type = tiledef[u, v] if should_remove: tiledef[u, v] = tiling.TileType.VOID tile_types.add(tile_type) if result_var: if tile_type.is_tile: # Don't distinguish between 4x4, goo sides inst.fixup[result_var] = tile_type.color.value elif tile_type is tiling.TileType.VOID: inst.fixup[result_var] = 'none' else: inst.fixup[result_var] = tile_type.name.casefold() return tile_type, both_colors, tile_types
def res_resizeable_trigger(vmf: VMF, res: Property): """Replace two markers with a trigger brush. This is run once to affect all of an item. Options: * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename. * `markerItem`: The item's ID * `previewConf`: A item config which enables/disables the preview overlay. * `previewInst`: An instance to place at the marker location in preview mode. This should contain checkmarks to display the value when testing. * `previewMat`: If set, the material to use for an overlay func_brush. The brush will be parented to the trigger, so it vanishes once killed. It is also non-solid. * `previewScale`: The scale for the func_brush materials. * `previewActivate`, `previewDeactivate`: The VMF output to turn the previewInst on and off. * `triggerActivate, triggerDeactivate`: The `instance:name;Output` outputs used when the trigger turns on or off. * `coopVar`: The instance variable which enables detecting both Coop players. The trigger will be a trigger_playerteam. * `coopActivate, coopDeactivate`: The `instance:name;Output` outputs used when coopVar is enabled. These should be suitable for a logic_coop_manager. * `coopOnce`: If true, kill the manager after it first activates. * `keys`: A block of keyvalues for the trigger brush. Origin and targetname will be set automatically. * `localkeys`: The same as above, except values will be changed to use instance-local names. """ marker = instanceLocs.resolve(res['markerInst']) marker_names = set() for inst in vmf.by_class['func_instance']: if inst['file'].casefold() in marker: marker_names.add(inst['targetname']) # Unconditionally delete from the map, so it doesn't # appear even if placed wrongly. inst.remove() if not marker_names: # No markers in the map - abort return RES_EXHAUSTED item_id = res['markerItem'] # Synthesise the item type used for the final trigger. item_type_sp = connections.ItemType( id=item_id + ':TRIGGER', output_act=Output.parse_name(res['triggerActivate', 'OnStartTouchAll']), output_deact=Output.parse_name(res['triggerDeactivate', 'OnEndTouchAll']), ) # For Coop, we add a logic_coop_manager in the mix so both players can # be handled. try: coop_var = res['coopVar'] except LookupError: coop_var = item_type_coop = None coop_only_once = False else: coop_only_once = res.bool('coopOnce') item_type_coop = connections.ItemType( id=item_id + ':TRIGGER_COOP', output_act=Output.parse_name( res['coopActivate', 'OnChangeToAllTrue'] ), output_deact=Output.parse_name( res['coopDeactivate', 'OnChangeToAnyFalse'] ), ) # Display preview overlays if it's preview mode, and the config is true pre_act = pre_deact = None if vbsp.IS_PREVIEW and vbsp_options.get_itemconf(res['previewConf', ''], False): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] preview_scale = res.float('previewScale', 0.25) # None if not found. with suppress(LookupError): pre_act = Output.parse(res.find_key('previewActivate')) with suppress(LookupError): pre_deact = Output.parse(res.find_key('previewDeactivate')) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' preview_scale = 0.25 # Now go through each brush. # We do while + pop to allow removing both names each loop through. todo_names = set(marker_names) while todo_names: targ = todo_names.pop() mark1 = connections.ITEMS.pop(targ) for conn in mark1.outputs: if conn.to_item.name in marker_names: mark2 = conn.to_item conn.remove() # Delete this connection. todo_names.discard(mark2.name) del connections.ITEMS[mark2.name] break else: if not mark1.inputs: # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. mark2 = mark1 else: # It's a marker with an input, the other in the pair # will handle everything. # But reinstate it in ITEMS. connections.ITEMS[targ] = mark1 continue inst1 = mark1.inst inst2 = mark2.inst is_coop = coop_var is not None and vbsp.GAME_MODE == 'COOP' and ( inst1.fixup.bool(coop_var) or inst2.fixup.bool(coop_var) ) bbox_min, bbox_max = Vec.bbox( Vec.from_str(inst1['origin']), Vec.from_str(inst2['origin']) ) origin = (bbox_max + bbox_min) / 2 # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vmf.create_ent( classname='trigger_multiple', # Default targetname=targ, origin=origin, angles='0 0 0', ) trig_ent.solids = [ vmf.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent = manager = vmf.create_ent( classname='logic_coop_manager', targetname=conditions.local_name(inst, 'man'), origin=origin, ) item = connections.Item( out_ent, item_type_coop, mark1.ant_floor_style, mark1.ant_wall_style, ) if coop_only_once: # Kill all the ents when both players are present. manager.add_out( Output('OnChangeToAllTrue', manager, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', manager, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', manager, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', manager, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', manager, 'SetStateBFalse'), ) else: item = connections.Item( trig_ent, item_type_sp, mark1.ant_floor_style, mark1.ant_wall_style, ) # Register, and copy over all the antlines. connections.ITEMS[item.name] = item item.ind_panels = mark1.ind_panels | mark2.ind_panels item.antlines = mark1.antlines | mark2.antlines item.shape_signs = mark1.shape_signs + mark2.shape_signs if preview_mat: preview_brush = vmf.create_ent( classname='func_brush', parentname=targ, origin=origin, Solidity='1', # Not solid drawinfastreflection='1', # Draw in goo.. # Disable shadows and lighting.. disableflashlight='1', disablereceiveshadows='1', disableshadowdepth='1', disableshadows='1', ) preview_brush.solids = [ # Make it slightly smaller, so it doesn't z-fight with surfaces. vmf.make_prism( bbox_min + 0.5, bbox_max - 0.5, mat=preview_mat, ).solid, ] for face in preview_brush.sides(): face.scale = preview_scale if preview_inst_file: pre_inst = vmf.create_ent( classname='func_instance', targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=inst2['origin'], ) if pre_act is not None: out = pre_act.copy() out.inst_out, out.output = item.output_act() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) if pre_deact is not None: out = pre_deact.copy() out.inst_out, out.output = item.output_deact() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) for conn in mark1.outputs | mark2.outputs: conn.from_item = item return RES_EXHAUSTED