def res_replace_instance(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. """ import vbsp origin = Vec.from_str(inst['origin']) angles = inst['angles'] if not srctools.conv_bool(res['keep_instance', '0'], False): 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' vbsp.VMF.add_ent(new_ent) conditions.set_ent_keys(new_ent, inst, res) origin += Vec.from_str(new_ent['origin']).rotate_by_str(angles) new_ent['origin'] = origin new_ent['angles'] = angles new_ent['targetname'] = inst['targetname']
def res_create_entity(vmf: VMF, inst: Entity, res: Property): """Create an entity. 'keys' and 'localkeys' defines the new keyvalues used. 'Origin' will be used to offset the given amount from the current location. """ origin = Vec.from_str(inst['origin']) new_ent = vmf.create_ent( # Ensure there's a classname, just in case. classname='info_null') conditions.set_ent_keys(new_ent, inst, res) origin += Vec.from_str(new_ent['origin']).rotate_by_str(inst['angles']) new_ent['origin'] = origin new_ent['angles'] = inst['angles']
def res_create_entity(vmf: VMF, inst: Entity, res: Property): """Create an entity. 'keys' and 'localkeys' defines the new keyvalues used. 'Origin' will be used to offset the given amount from the current location. """ origin = Vec.from_str(inst['origin']) new_ent = vmf.create_ent( # Ensure there's a classname, just in case. classname='info_null' ) conditions.set_ent_keys(new_ent, inst, res) origin += Vec.from_str(new_ent['origin']).rotate_by_str(inst['angles']) new_ent['origin'] = origin new_ent['angles'] = inst['angles']
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_antlaser(vmf: VMF, res: Property): """The condition to generate AntLasers. This is executed once to modify all instances. """ conf_inst = instanceLocs.resolve(res['instance']) conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64) conf_las_start = Vec(z=res.float('LasStart') - 64) conf_rope_off = res.vec('RopePos') conf_toggle_targ = res['toggleTarg', ''] beam_conf = res.find_key('BeamKeys', []) glow_conf = res.find_key('GlowKeys', []) cable_conf = res.find_key('CableKeys', []) if beam_conf: # Grab a copy of the beam spawnflags so we can set our own options. conf_beam_flags = beam_conf.int('spawnflags') # Mask out certain flags. conf_beam_flags &= ( 0 | 1 # Start On | 2 # Toggle | 4 # Random Strike | 8 # Ring | 16 # StartSparks | 32 # EndSparks | 64 # Decal End #| 128 # Shade Start #| 256 # Shade End #| 512 # Taper Out ) else: conf_beam_flags = 0 conf_outputs = [ Output.parse(prop) for prop in res if prop.name in ('onenabled', 'ondisabled') ] # Find all the markers. nodes = {} # type: Dict[str, Item] for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in conf_inst: continue name = inst['targetname'] try: # Remove the item - it's no longer going to exist after # we're done. nodes[name] = connections.ITEMS.pop(name) except KeyError: raise ValueError('No item for "{}"?'.format(name)) from None if not nodes: # None at all. return conditions.RES_EXHAUSTED # Now find every connected group, recording inputs, outputs and links. todo = set(nodes.values()) groups = [] # type: List[Group] # Node -> is grouped already. node_pairing = dict.fromkeys(nodes.values(), False) while todo: start = todo.pop() # Synthesise the Item used for logic. # We use a random info_target to manage the IO data. group = Group(start) groups.append(group) for node in group.nodes: # If this node has no non-node outputs, destroy the antlines. has_output = False node_pairing[node] = True for conn in list(node.outputs): neighbour = conn.to_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, a target of our logic. conn.from_item = group.item has_output = True continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({node, neighbour})) # If we have a real output, we need to transfer it. # Otherwise we can just destroy it. if has_output: node.transfer_antlines(group.item) else: node.delete_antlines() # Do the same for inputs, so we can catch that. for conn in list(node.inputs): neighbour = conn.from_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, an input to the group. conn.to_item = group.item continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({neighbour, node})) # Now every node is in a group. Generate the actual entities. for group in groups: # We generate two ent types. For each marker, we add a sprite # and a beam pointing at it. Then for each connection # another beam. # Choose a random antlaser name to use for our group. base_name = group.nodes[0].name out_enable = [Output('', '', 'FireUser2')] out_disable = [Output('', '', 'FireUser1')] for output in conf_outputs: if output.output.casefold() == 'onenabled': out_enable.append(output.copy()) else: out_disable.append(output.copy()) if conf_toggle_targ: # Make the group info_target into a texturetoggle. toggle = group.item.inst toggle['classname'] = 'env_texturetoggle' toggle['target'] = conditions.local_name(group.nodes[0].inst, conf_toggle_targ) group.item.enable_cmd = tuple(out_enable) group.item.disable_cmd = tuple(out_disable) # Node -> index for targetnames. indexes = {} # type: Dict[Item, int] # For cables, it's a bit trickier than the beams. # The cable ent itself is the one which decides what it links to, # so we need to potentially make endpoint cables at locations with # only "incoming" lines. # So this dict is either a targetname to indicate cables with an # outgoing connection, or the entity for endpoints without an outgoing # connection. cable_points = {} # type: Dict[Item, Union[Entity, str]] for i, node in enumerate(group.nodes, start=1): indexes[node] = i node.name = base_name sprite_pos = conf_glow_height.copy() sprite_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) if glow_conf: # First add the sprite at the right height. sprite = vmf.create_ent('env_sprite') for prop in glow_conf: sprite[prop.name] = conditions.resolve_value(node.inst, prop.value) sprite['origin'] = sprite_pos sprite['targetname'] = NAME_SPR(base_name, i) elif beam_conf: # If beams but not sprites, we need a target. vmf.create_ent( 'info_target', origin=sprite_pos, targetname=NAME_SPR(base_name, i), ) if beam_conf: # Now the beam going from below up to the sprite. beam_pos = conf_las_start.copy() beam_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) beam = vmf.create_ent('env_beam') for prop in beam_conf: beam[prop.name] = conditions.resolve_value(node.inst, prop.value) beam['origin'] = beam['targetpoint'] = beam_pos beam['targetname'] = NAME_BEAM_LOW(base_name, i) beam['LightningStart'] = beam['targetname'] beam['LightningEnd'] = NAME_SPR(base_name, i) beam['spawnflags'] = conf_beam_flags | 128 # Shade Start if beam_conf: for i, (node_a, node_b) in enumerate(group.links): beam = vmf.create_ent('env_beam') conditions.set_ent_keys(beam, node_a.inst, res, 'BeamKeys') beam['origin'] = beam['targetpoint'] = node_a.inst['origin'] beam['targetname'] = NAME_BEAM_CONN(base_name, i) beam['LightningStart'] = NAME_SPR(base_name, indexes[node_a]) beam['LightningEnd'] = NAME_SPR(base_name, indexes[node_b]) beam['spawnflags'] = conf_beam_flags # We have a couple different situations to deal with here. # Either end could Not exist, be Unlinked, or be Linked = 8 combos. # Always flip so we do A to B. # AB | # NN | Make 2 new ones, one is an endpoint. # NU | Flip, do UN. # NL | Make A, link A to B. Both are linked. # UN | Make B, link A to B. B is unlinked. # UU | Link A to B, A is now linked, B is unlinked. # UL | Link A to B. Both are linked. # LN | Flip, do NL. # LU | Flip, do UL # LL | Make A, link A to B. Both are linked. if cable_conf: rope_ind = 0 # Uniqueness value. for node_a, node_b in group.links: state_a, ent_a = RopeState.from_node(cable_points, node_a) state_b, ent_b = RopeState.from_node(cable_points, node_b) if (state_a is RopeState.LINKED or (state_a is RopeState.NONE and state_b is RopeState.UNLINKED) ): # Flip these, handle the opposite order. state_a, state_b = state_b, state_a ent_a, ent_b = ent_b, ent_a node_a, node_b = node_b, node_a pos_a = conf_rope_off.copy() pos_a.localise( Vec.from_str(node_a.inst['origin']), Vec.from_str(node_a.inst['angles']), ) pos_b = conf_rope_off.copy() pos_b.localise( Vec.from_str(node_b.inst['origin']), Vec.from_str(node_b.inst['angles']), ) # Need to make the A rope if we don't have one that's unlinked. if state_a is not RopeState.UNLINKED: rope_a = vmf.create_ent('move_rope') for prop in beam_conf: rope_a[prop.name] = conditions.resolve_value(node_a.inst, prop.value) rope_a['origin'] = pos_a rope_ind += 1 rope_a['targetname'] = NAME_CABLE(base_name, rope_ind) else: # It is unlinked, so it's the rope to use. rope_a = ent_a # Only need to make the B rope if it doesn't have one. if state_b is RopeState.NONE: rope_b = vmf.create_ent('move_rope') for prop in beam_conf: rope_b[prop.name] = conditions.resolve_value(node_b.inst, prop.value) rope_b['origin'] = pos_b rope_ind += 1 name_b = rope_b['targetname'] = NAME_CABLE(base_name, rope_ind) cable_points[node_b] = rope_b # Someone can use this. elif state_b is RopeState.UNLINKED: # Both must be unlinked, we aren't using this link though. name_b = ent_b['targetname'] else: # Linked, we just have the name. name_b = ent_b # By here, rope_a should be an unlinked rope, # and name_b should be a name to link to. rope_a['nextkey'] = name_b # Figure out how much slack to give. # If on floor, we need to be taut to have clearance. if on_floor(node_a) or on_floor(node_b): rope_a['slack'] = 60 else: rope_a['slack'] = 125 # We're always linking A to B, so A is always linked! if state_a is not RopeState.LINKED: cable_points[node_a] = rope_a['targetname'] return conditions.RES_EXHAUSTED
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 res_import_template(inst: Entity, res: Property): """Import a template VMF file, retexturing it to match orientation. It will be placed overlapping the given instance. Options: - ID: The ID of the template to be inserted. Add visgroups to additionally add after a colon, comma-seperated (temp_id:vis1,vis2) - 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. - replaceBrush: The position of a brush to replace (0 0 0=the surface). This brush will be removed, and overlays will be fixed to use all faces with the same normal. Can alternately be a block: - Pos: The position to replace. - additionalIDs: Space-separated list of face IDs in the template to also fix for overlays. The surface should have close to a vertical normal, to prevent rescaling the overlay. - removeBrush: If true, the original brush will not be removed. - transferOverlay: Allow disabling transferring overlays to this template. The IDs will be removed instead. (This can be an instvar). - 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. If 'none' (default), they are ignored. If 'choose', one is chosen. If a number, that is the percentage chance for each visgroup to be added. - visgroup_force_var: If set and True, visgroup is ignored and all groups are added. """ ( orig_temp_id, replace_tex, force_colour, force_grid, force_type, replace_brush_pos, rem_replace_brush, transfer_overlays, additional_replace_ids, invert_var, color_var, visgroup_func, visgroup_force_var, key_block, ) = res.value temp_id = conditions.resolve_value(inst, orig_temp_id) if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)): def visgroup_func(group): """Use all the groups.""" yield from group temp_name, visgroups = template_brush.parse_temp_name(temp_id) try: template = template_brush.get_template(temp_name) except template_brush.InvalidTemplateName: # The template map is read in after setup is performed, so # it must be checked here! # We don't want an error, just quit 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) return if color_var.casefold() == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = template_brush.MAT_TYPES.white elif 'black' in traits: force_colour = template_brush.MAT_TYPES.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 = template_brush.MAT_TYPES.white elif color_val == 'black': force_colour = template_brush.MAT_TYPES.black # else: no color var if srctools.conv_bool(conditions.resolve_value(inst, invert_var)): force_colour = template_brush.TEMP_COLOUR_INVERT[force_colour] # else: False value, no invert. origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles', '0 0 0']) temp_data = template_brush.import_template( template, origin, angles, targetname=inst['targetname', ''], force_type=force_type, visgroup_choose=visgroup_func, add_to_map=True, additional_visgroups=visgroups, ) 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, angles) 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).rotate(*angles) temp_data.detail['movedir'] = move_dir.to_angle() # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't # modify it. vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail) try: # This is the original brush the template is replacing. We fix overlay # face IDs, so this brush is replaced by the faces in the template # pointing # the same way. if replace_brush_pos is None: raise KeyError # Not set, raise to jump out of the try block pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z) pos += origin brush_group = SOLIDS[pos.as_tuple()] except KeyError: # Not set or solid group doesn't exist, skip.. pass else: LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces) conditions.steal_from_brush( temp_data, brush_group, rem_replace_brush, map(int, additional_replace_ids | template.overlay_faces), conv_bool(conditions.resolve_value(inst, transfer_overlays), True), ) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, )
def res_import_template(inst: Entity, res: Property): """Import a template VMF file, retexturing it to match orientation. It will be placed overlapping the given instance. 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`. - `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 face ID. - `replaceBrush`: The position of a brush to replace (`0 0 0`=the surface). This brush will be removed, and overlays will be fixed to use all faces with the same normal. Can alternately be a block: - `Pos`: The position to replace. - `additionalIDs`: Space-separated list of face IDs in the template to also fix for overlays. The surface should have close to a vertical normal, to prevent rescaling the overlay. - `removeBrush`: If true, the original brush will not be removed. - `transferOverlay`: Allow disabling transferring overlays to this template. The IDs will be removed instead. (This can be a `$fixup`). - `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. If `none` (default), they are ignored. If `choose`, one is chosen. If a number, that is the percentage chance for each visgroup to be added. - `visgroup_force_var`: If set and True, visgroup is ignored and all groups are added. - `outputs`: Add outputs to the brush ent. Syntax is like VMFs, and all names are local to the instance. """ ( orig_temp_id, replace_tex, force_colour, force_grid, force_type, replace_brush_pos, rem_replace_brush, transfer_overlays, additional_replace_ids, invert_var, color_var, visgroup_func, visgroup_force_var, key_block, outputs, ) = res.value if ':' in orig_temp_id: # Split, resolve each part, then recombine. temp_id, visgroup = orig_temp_id.split(':', 1) temp_id = ( conditions.resolve_value(inst, temp_id) + ':' + conditions.resolve_value(inst, visgroup) ) else: temp_id = conditions.resolve_value(inst, orig_temp_id) if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)): def visgroup_func(group): """Use all the groups.""" yield from group 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 if color_var.casefold() == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = template_brush.MAT_TYPES.white elif 'black' in traits: force_colour = template_brush.MAT_TYPES.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 = template_brush.MAT_TYPES.white elif color_val == 'black': force_colour = template_brush.MAT_TYPES.black # else: no color var if srctools.conv_bool(conditions.resolve_value(inst, invert_var)): force_colour = template_brush.TEMP_COLOUR_INVERT[force_colour] # else: False value, no invert. origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles', '0 0 0']) temp_data = template_brush.import_template( template, origin, angles, targetname=inst['targetname', ''], force_type=force_type, visgroup_choose=visgroup_func, add_to_map=True, additional_visgroups=visgroups, ) 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, angles) 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).rotate(*angles) temp_data.detail['movedir'] = move_dir.to_angle() for out in outputs: # type: Output out = out.copy() out.target = conditions.local_name(inst, out.target) temp_data.detail.add_out(out) # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't # modify it. vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail) try: # This is the original brush the template is replacing. We fix overlay # face IDs, so this brush is replaced by the faces in the template # pointing # the same way. if replace_brush_pos is None: raise KeyError # Not set, raise to jump out of the try block pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z) pos += origin brush_group = SOLIDS[pos.as_tuple()] except KeyError: # Not set or solid group doesn't exist, skip.. pass else: LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces) conditions.steal_from_brush( temp_data, brush_group, rem_replace_brush, map(int, additional_replace_ids | template.overlay_faces), conv_bool(conditions.resolve_value(inst, transfer_overlays), True), ) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, # Don't allow clumping if using custom keyvalues - then it won't be edited. no_clumping=key_block is not None, )
def res_fix_rotation_axis(ent, res): """Generate a `func_rotating`, `func_door_rotating` or any similar entity. This uses the orientation of the instance to detemine the correct spawnflags to make it rotate in the correct direction. The brush will be 2x2x2 units large, and always set to be non-solid. - `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`, `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_rotating` * `func_door_rotating` * `func_rot_button` * `func_platrot` """ des_axis = res['axis', 'z'].casefold() reverse = utils.conv_bool(res['reversed', '0']) door_type = res['classname', 'func_door_rotating'] # Extra stuff to apply to the flags (USE, toggle, etc) flags = sum(map( # Add together multiple values utils.conv_int, res['flags', '0'].split('+') )) name = res['name', ''] if not name.startswith('@'): # If a local name is given, add it to the instance targetname. # It the name given is '', set to the instance's name. # If it has an @, don't change it! name = ent['targetname', ''] + (('-' + name) if name else '') axis = Vec( x=int(des_axis == 'x'), y=int(des_axis == 'y'), z=int(des_axis == 'z'), ).rotate_by_str(ent['angles', '0 0 0']) pos = Vec.from_str( res['Pos', '0 0 0'] ).rotate_by_str(ent['angles', '0 0 0']) pos += Vec.from_str(ent['origin', '0 0 0']) door_ent = vbsp.VMF.create_ent( classname=door_type, targetname=name, origin=pos.join(' '), ) conditions.set_ent_keys(door_ent, ent, res) for output in res.find_all('AddOut'): door_ent.add_out(VLib.Output( out=output['Output', 'OnUse'], inp=output['Input', 'Use'], targ=output['Target', ''], inst_in=output['Inst_targ', None], param=output['Param', ''], delay=utils.conv_float(output['Delay', '']), times=( 1 if utils.conv_bool(output['OnceOnly', False]) else -1), )) # Generate brush door_ent.solids = [vbsp.VMF.make_prism(pos - 1, pos + 1).solid] 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 flag_values = FLAG_ROTATING[door_type] # Make the door always non-solid! flags |= flag_values.get('solid_flags', 0) # Add or remove flags as needed. # flags |= bit sets it to 1. # flags |= ~bit sets it to 0. if axis.x != 0: flags |= flag_values.get('x', 0) else: flags &= ~flag_values.get('x', 0) if axis.y != 0: flags |= flag_values.get('y', 0) else: flags &= ~flag_values.get('y', 0) if axis.z != 0: flags |= flag_values.get('z', 0) else: flags &= ~flag_values.get('z', 0) if door_type == 'momentary_rot_button': door_ent['startdirection'] = '1' if reverse else '-1' else: if reverse: flags |= flag_values.get('rev', 0) else: flags &= ~flag_values.get('rev', 0) door_ent['spawnflags'] = str(flags)
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_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_antlaser(vmf: VMF, res: Property): """The condition to generate AntLasers. This is executed once to modify all instances. """ conf_inst = instanceLocs.resolve(res['instance']) conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64) conf_las_start = Vec(z=res.float('LasStart') - 64) # Grab a copy of the beam spawnflags so we can set our own options. conf_beam_flags = res.find_key('BeamKeys', []).int('spawnflags') # Mask out certain flags. conf_beam_flags &= ( 0 | 1 # Start On | 2 # Toggle | 4 # Random Strike | 8 # Ring | 16 # StartSparks | 32 # EndSparks | 64 # Decal End #| 128 # Shade Start #| 256 # Shade End #| 512 # Taper Out ) conf_outputs = [ Output.parse(prop) for prop in res if prop.name in ('onenabled', 'ondisabled') ] # Find all the markers. nodes = {} # type: Dict[str, Item] for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in conf_inst: continue name = inst['targetname'] try: # Remove the item - it's no longer going to exist after # we're done. nodes[name] = connections.ITEMS.pop(name) except KeyError: raise ValueError('No item for "{}"?'.format(name)) from None # Now find every connected group, recording inputs, outputs and links. todo = set(nodes.values()) groups = [] # type: List[Group] # Node -> is grouped already. node_pairing = dict.fromkeys(nodes.values(), False) while todo: start = todo.pop() # Synthesise the Item used for logic. # We use a random info_target to manage the IO data. group = Group(start) groups.append(group) for node in group.nodes: # If this node has no non-node outputs, destroy the antlines. has_output = False node_pairing[node] = True for conn in list(node.outputs): neighbour = conn.to_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, a target of our logic. conn.from_item = group.item has_output = True continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add((node, neighbour)) # If we have a real output, we need to transfer it. # Otherwise we can just destroy it. if has_output: group.item.antlines.update(node.antlines) group.item.ind_panels.update(node.ind_panels) group.item.shape_signs.extend(node.shape_signs) else: node.delete_antlines() # Do the same for inputs, so we can catch that. for conn in list(node.inputs): neighbour = conn.from_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, an input to the group. conn.to_item = group.item continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add((neighbour, node)) # Now every node is in a group. Generate the actual entities. for group in groups: # We generate two ent types. For each marker, we add a sprite # and a beam pointing at it. Then for each connection # another beam. # Choose a random antlaser name to use for our group. base_name = group.nodes[0].name out_enable = [Output('', '', 'FireUser2')] out_disable = [Output('', '', 'FireUser1')] for output in conf_outputs: if output.output.casefold() == 'onenabled': out_enable.append(output.copy()) else: out_disable.append(output.copy()) group.item.enable_cmd = tuple(out_enable) group.item.disable_cmd = tuple(out_disable) # Node -> index for targetnames. indexes = {} # type: Dict[Item, int] for i, node in enumerate(group.nodes, start=1): indexes[node] = i node.name = base_name # First add the sprite at the right height. sprite_pos = conf_glow_height.copy() sprite_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) sprite = vmf.create_ent('env_sprite') conditions.set_ent_keys(sprite, node.inst, res, 'GlowKeys') sprite['origin'] = sprite_pos sprite['targetname'] = NAME_SPR(base_name, i) # Now the beam going from below up to the sprite. beam_pos = conf_las_start.copy() beam_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) beam = vmf.create_ent('env_beam') conditions.set_ent_keys(beam, node.inst, res, 'BeamKeys') beam['origin'] = beam_pos beam['targetname'] = NAME_BEAM_LOW(base_name, i) beam['LightningStart'] = beam['targetname'] beam['LightningEnd'] = NAME_SPR(base_name, i) beam['spawnflags'] = conf_beam_flags | 128 # Shade Start for i, (node1, node2) in enumerate(group.links): beam = vmf.create_ent('env_beam') conditions.set_ent_keys(beam, node1.inst, res, 'BeamKeys') beam['origin'] = node1.inst['origin'] beam['targetname'] = NAME_BEAM_CONN(base_name, i) beam['LightningStart'] = NAME_SPR(base_name, indexes[node1]) beam['LightningEnd'] = NAME_SPR(base_name, indexes[node2]) beam['spawnflags'] = conf_beam_flags return conditions.RES_EXHAUSTED
def res_fizzler_pair(vmf: VMF, begin_inst: Entity, res: Property): """Modify the instance of a fizzler to link with its pair. Each pair will be given a name along the lines of "fizz_name-model1334". Values: - StartInst, EndInst: The instances used for each end - MidInst: An instance placed every 128 units between emitters. - SingleInst: If the models are 1 block apart, replace both with this instance. - BrushKeys, LocalBrushKeys: If specified, a brush entity will be generated from some templates at the position of the models. - StartTemp, EndTemp, SingleTemp: Templates for the above. - SingleBrush: If true, the brush will be shared among the entirety of this fizzler. - uniqueName: If true, all pairs get a unique name for themselves. if False, all instances use the base instance name. """ orig_target = begin_inst['targetname'] if 'modelEnd' in orig_target: return # We only execute starting from the start side. orig_target = orig_target[:-11] # remove "_modelStart" end_name = orig_target + '_modelEnd' # What we search for # The name all these instances get if srctools.conv_bool(res['uniqueName', '1'], True): pair_name = orig_target + '-model' + str(begin_inst.id) else: pair_name = orig_target orig_file = begin_inst['file'] begin_inst['file'] = instanceLocs.resolve_one(res['StartInst'], error=True) end_file = instanceLocs.resolve_one(res['EndInst'], error=True) mid_file = instanceLocs.resolve_one(res['MidInst', '']) single_file = instanceLocs.resolve_one(res['SingleInst', '']) begin_inst['targetname'] = pair_name brush = None if 'brushkeys' in res: begin_temp = res['StartTemp', ''] end_temp = res['EndTemp', ''] single_temp = res['SingleTemp'] if res.bool('SingleBrush'): try: brush = PAIR_FIZZ_BRUSHES[orig_target] except KeyError: pass if not brush: brush = vmf.create_ent( classname='func_brush', # default origin=begin_inst['origin'], ) conditions.set_ent_keys( brush, begin_inst, res, 'BrushKeys', ) if res.bool('SingleBrush'): PAIR_FIZZ_BRUSHES[orig_target] = brush else: begin_temp = end_temp = single_temp = None direction = Vec(0, 0, 1).rotate_by_str(begin_inst['angles']) begin_pos = Vec.from_str(begin_inst['origin']) axis_1, axis_2, main_axis = PAIR_AXES[direction.as_tuple()] for end_inst in vbsp.VMF.by_class['func_instance']: if end_inst['targetname', ''] != end_name: # Only examine this barrier hazard's instances! continue if end_inst['file'] != orig_file: # Allow adding overlays or other instances at the ends. continue end_pos = Vec.from_str(end_inst['origin']) if (begin_pos[axis_1] == end_pos[axis_1] and begin_pos[axis_2] == end_pos[axis_2]): length = int(end_pos[main_axis] - begin_pos[main_axis]) break else: LOGGER.warning('No matching pair for {}!!', orig_target) return if length == 0: if single_temp: temp_brushes = template_brush.import_template( single_temp, Vec.from_str(begin_inst['origin']), Vec.from_str(begin_inst['angles']), force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) brush.solids.extend(temp_brushes.world) if single_file: end_inst.remove() begin_inst['file'] = single_file # Don't do anything else with end instances. return else: if begin_temp: temp_brushes = template_brush.import_template( begin_temp, Vec.from_str(begin_inst['origin']), Vec.from_str(begin_inst['angles']), force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) brush.solids.extend(temp_brushes.world) if end_temp: temp_brushes = template_brush.import_template( end_temp, Vec.from_str(end_inst['origin']), Vec.from_str(end_inst['angles']), force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) brush.solids.extend(temp_brushes.world) end_inst['targetname'] = pair_name end_inst['file'] = end_file if mid_file != '' and length: # Go 64 from each side, and always have at least 1 section # A 128 gap will have length = 0 for dis in range(0, abs(length) + 1, 128): new_pos = begin_pos + direction * dis vbsp.VMF.create_ent( classname='func_instance', targetname=pair_name, angles=begin_inst['angles'], file=mid_file, origin=new_pos, )
def res_import_template(inst: Entity, res: Property): """Import a template VMF file, retexturing it to match orientation. It will be placed overlapping the given instance. Options: - ID: The ID of the template to be inserted. Add visgroups to additionally add after a colon, comma-seperated (temp_id:vis1,vis2) - 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 face ID. - replaceBrush: The position of a brush to replace (0 0 0=the surface). This brush will be removed, and overlays will be fixed to use all faces with the same normal. Can alternately be a block: - Pos: The position to replace. - additionalIDs: Space-separated list of face IDs in the template to also fix for overlays. The surface should have close to a vertical normal, to prevent rescaling the overlay. - removeBrush: If true, the original brush will not be removed. - transferOverlay: Allow disabling transferring overlays to this template. The IDs will be removed instead. (This can be an instvar). - 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. If 'none' (default), they are ignored. If 'choose', one is chosen. If a number, that is the percentage chance for each visgroup to be added. - visgroup_force_var: If set and True, visgroup is ignored and all groups are added. - outputs: Add outputs to the brush ent. Syntax is like VMFs, and all names are local to the instance. """ ( orig_temp_id, replace_tex, force_colour, force_grid, force_type, replace_brush_pos, rem_replace_brush, transfer_overlays, additional_replace_ids, invert_var, color_var, visgroup_func, visgroup_force_var, key_block, outputs, ) = res.value if ':' in orig_temp_id: # Split, resolve each part, then recombine. temp_id, visgroup = orig_temp_id.split(':', 1) temp_id = ( conditions.resolve_value(inst, temp_id) + ':' + conditions.resolve_value(inst, visgroup) ) else: temp_id = conditions.resolve_value(inst, orig_temp_id) if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)): def visgroup_func(group): """Use all the groups.""" yield from group 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 if color_var.casefold() == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = template_brush.MAT_TYPES.white elif 'black' in traits: force_colour = template_brush.MAT_TYPES.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 = template_brush.MAT_TYPES.white elif color_val == 'black': force_colour = template_brush.MAT_TYPES.black # else: no color var if srctools.conv_bool(conditions.resolve_value(inst, invert_var)): force_colour = template_brush.TEMP_COLOUR_INVERT[force_colour] # else: False value, no invert. origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles', '0 0 0']) temp_data = template_brush.import_template( template, origin, angles, targetname=inst['targetname', ''], force_type=force_type, visgroup_choose=visgroup_func, add_to_map=True, additional_visgroups=visgroups, ) 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, angles) 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).rotate(*angles) temp_data.detail['movedir'] = move_dir.to_angle() for out in outputs: # type: Output out = out.copy() out.target = conditions.local_name(inst, out.target) temp_data.detail.add_out(out) # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't # modify it. vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail) try: # This is the original brush the template is replacing. We fix overlay # face IDs, so this brush is replaced by the faces in the template # pointing # the same way. if replace_brush_pos is None: raise KeyError # Not set, raise to jump out of the try block pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z) pos += origin brush_group = SOLIDS[pos.as_tuple()] except KeyError: # Not set or solid group doesn't exist, skip.. pass else: LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces) conditions.steal_from_brush( temp_data, brush_group, rem_replace_brush, map(int, additional_replace_ids | template.overlay_faces), conv_bool(conditions.resolve_value(inst, transfer_overlays), True), ) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, # Don't allow clumping if using custom keyvalues - then it won't be edited. no_clumping=key_block is not None, )
def res_antlaser(vmf: VMF, res: Property): """The condition to generate AntLasers. This is executed once to modify all instances. """ conf_inst = instanceLocs.resolve(res['instance']) conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64) conf_las_start = Vec(z=res.float('LasStart') - 64) conf_rope_off = res.vec('RopePos') conf_toggle_targ = res['toggleTarg', ''] beam_conf = res.find_key('BeamKeys', []) glow_conf = res.find_key('GlowKeys', []) cable_conf = res.find_key('CableKeys', []) if beam_conf: # Grab a copy of the beam spawnflags so we can set our own options. conf_beam_flags = beam_conf.int('spawnflags') # Mask out certain flags. conf_beam_flags &= ( 0 | 1 # Start On | 2 # Toggle | 4 # Random Strike | 8 # Ring | 16 # StartSparks | 32 # EndSparks | 64 # Decal End #| 128 # Shade Start #| 256 # Shade End #| 512 # Taper Out ) else: conf_beam_flags = 0 conf_outputs = [ Output.parse(prop) for prop in res if prop.name in ('onenabled', 'ondisabled') ] # Find all the markers. nodes = {} # type: Dict[str, Item] for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in conf_inst: continue name = inst['targetname'] try: # Remove the item - it's no longer going to exist after # we're done. nodes[name] = connections.ITEMS.pop(name) except KeyError: raise ValueError('No item for "{}"?'.format(name)) from None if not nodes: # None at all. return conditions.RES_EXHAUSTED # Now find every connected group, recording inputs, outputs and links. todo = set(nodes.values()) groups = [] # type: List[Group] # Node -> is grouped already. node_pairing = dict.fromkeys(nodes.values(), False) while todo: start = todo.pop() # Synthesise the Item used for logic. # We use a random info_target to manage the IO data. group = Group(start) groups.append(group) for node in group.nodes: # If this node has no non-node outputs, destroy the antlines. has_output = False node_pairing[node] = True for conn in list(node.outputs): neighbour = conn.to_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, a target of our logic. conn.from_item = group.item has_output = True continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({node, neighbour})) # If we have a real output, we need to transfer it. # Otherwise we can just destroy it. if has_output: node.transfer_antlines(group.item) else: node.delete_antlines() # Do the same for inputs, so we can catch that. for conn in list(node.inputs): neighbour = conn.from_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, an input to the group. conn.to_item = group.item continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({neighbour, node})) # Now every node is in a group. Generate the actual entities. for group in groups: # We generate two ent types. For each marker, we add a sprite # and a beam pointing at it. Then for each connection # another beam. # Choose a random antlaser name to use for our group. base_name = group.nodes[0].name out_enable = [Output('', '', 'FireUser2')] out_disable = [Output('', '', 'FireUser1')] for output in conf_outputs: if output.output.casefold() == 'onenabled': out_enable.append(output.copy()) else: out_disable.append(output.copy()) if conf_toggle_targ: # Make the group info_target into a texturetoggle. toggle = group.item.inst toggle['classname'] = 'env_texturetoggle' toggle['target'] = conditions.local_name(group.nodes[0].inst, conf_toggle_targ) group.item.enable_cmd = tuple(out_enable) group.item.disable_cmd = tuple(out_disable) # Node -> index for targetnames. indexes = {} # type: Dict[Item, int] # For cables, it's a bit trickier than the beams. # The cable ent itself is the one which decides what it links to, # so we need to potentially make endpoint cables at locations with # only "incoming" lines. # So this dict is either a targetname to indicate cables with an # outgoing connection, or the entity for endpoints without an outgoing # connection. cable_points = {} # type: Dict[Item, Union[Entity, str]] for i, node in enumerate(group.nodes, start=1): indexes[node] = i node.name = base_name sprite_pos = conf_glow_height.copy() sprite_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) if glow_conf: # First add the sprite at the right height. sprite = vmf.create_ent('env_sprite') for prop in glow_conf: sprite[prop.name] = conditions.resolve_value( node.inst, prop.value) sprite['origin'] = sprite_pos sprite['targetname'] = NAME_SPR(base_name, i) elif beam_conf: # If beams but not sprites, we need a target. vmf.create_ent( 'info_target', origin=sprite_pos, targetname=NAME_SPR(base_name, i), ) if beam_conf: # Now the beam going from below up to the sprite. beam_pos = conf_las_start.copy() beam_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) beam = vmf.create_ent('env_beam') for prop in beam_conf: beam[prop.name] = conditions.resolve_value( node.inst, prop.value) beam['origin'] = beam['targetpoint'] = beam_pos beam['targetname'] = NAME_BEAM_LOW(base_name, i) beam['LightningStart'] = beam['targetname'] beam['LightningEnd'] = NAME_SPR(base_name, i) beam['spawnflags'] = conf_beam_flags | 128 # Shade Start if beam_conf: for i, (node_a, node_b) in enumerate(group.links): beam = vmf.create_ent('env_beam') conditions.set_ent_keys(beam, node_a.inst, res, 'BeamKeys') beam['origin'] = beam['targetpoint'] = node_a.inst['origin'] beam['targetname'] = NAME_BEAM_CONN(base_name, i) beam['LightningStart'] = NAME_SPR(base_name, indexes[node_a]) beam['LightningEnd'] = NAME_SPR(base_name, indexes[node_b]) beam['spawnflags'] = conf_beam_flags # We have a couple different situations to deal with here. # Either end could Not exist, be Unlinked, or be Linked = 8 combos. # Always flip so we do A to B. # AB | # NN | Make 2 new ones, one is an endpoint. # NU | Flip, do UN. # NL | Make A, link A to B. Both are linked. # UN | Make B, link A to B. B is unlinked. # UU | Link A to B, A is now linked, B is unlinked. # UL | Link A to B. Both are linked. # LN | Flip, do NL. # LU | Flip, do UL # LL | Make A, link A to B. Both are linked. if cable_conf: rope_ind = 0 # Uniqueness value. for node_a, node_b in group.links: state_a, ent_a = RopeState.from_node(cable_points, node_a) state_b, ent_b = RopeState.from_node(cable_points, node_b) if (state_a is RopeState.LINKED or (state_a is RopeState.NONE and state_b is RopeState.UNLINKED)): # Flip these, handle the opposite order. state_a, state_b = state_b, state_a ent_a, ent_b = ent_b, ent_a node_a, node_b = node_b, node_a pos_a = conf_rope_off.copy() pos_a.localise( Vec.from_str(node_a.inst['origin']), Vec.from_str(node_a.inst['angles']), ) pos_b = conf_rope_off.copy() pos_b.localise( Vec.from_str(node_b.inst['origin']), Vec.from_str(node_b.inst['angles']), ) # Need to make the A rope if we don't have one that's unlinked. if state_a is not RopeState.UNLINKED: rope_a = vmf.create_ent('move_rope') for prop in beam_conf: rope_a[prop.name] = conditions.resolve_value( node_a.inst, prop.value) rope_a['origin'] = pos_a rope_ind += 1 rope_a['targetname'] = NAME_CABLE(base_name, rope_ind) else: # It is unlinked, so it's the rope to use. rope_a = ent_a # Only need to make the B rope if it doesn't have one. if state_b is RopeState.NONE: rope_b = vmf.create_ent('move_rope') for prop in beam_conf: rope_b[prop.name] = conditions.resolve_value( node_b.inst, prop.value) rope_b['origin'] = pos_b rope_ind += 1 name_b = rope_b['targetname'] = NAME_CABLE( base_name, rope_ind) cable_points[node_b] = rope_b # Someone can use this. elif state_b is RopeState.UNLINKED: # Both must be unlinked, we aren't using this link though. name_b = ent_b['targetname'] else: # Linked, we just have the name. name_b = ent_b # By here, rope_a should be an unlinked rope, # and name_b should be a name to link to. rope_a['nextkey'] = name_b # Figure out how much slack to give. # If on floor, we need to be taut to have clearance. if on_floor(node_a) or on_floor(node_b): rope_a['slack'] = 60 else: rope_a['slack'] = 125 # We're always linking A to B, so A is always linked! if state_a is not RopeState.LINKED: cable_points[node_a] = rope_a['targetname'] return conditions.RES_EXHAUSTED
def res_import_template(inst, res): """Import a template VMF file, retexturing it to match orientatation. It will be placed overlapping the given instance. Options: - ID: The ID of the template to be inserted. - force: a space-seperated list of overrides. If 'white' or 'black' is present, the colour of tiles will be overriden. 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. - replaceBrush: The position of a brush to replace (0 0 0=the surface). This brush will be removed, and overlays will be fixed to use all faces with the same normal. - keys/localkeys: If set, a brush entity will instead be generated with these values. This overrides force world/detail. The origin is set automatically. - invertVar: If this fixup value is true, tile colour will be swapped to the opposite of the current force option. If it is set to 'white' or 'black', that colour will be forced instead. """ ( temp_id, replace_tex, force_colour, force_grid, force_type, replace_brush_pos, invert_var, key_block, ) = res.value if temp_id not in TEMPLATES: # The template map is read in after setup is performed, so # it must be checked here! # We don't want an error, just quit LOGGER.warning('"{}" not a valid template!', temp_id) return if invert_var != '': invert_val = inst.fixup[invert_var].casefold() if invert_val == 'white': force_colour = conditions.MAT_TYPES.white elif invert_val == 'black': force_colour = conditions.MAT_TYPES.black elif utils.conv_bool(invert_val): force_colour = conditions.TEMP_COLOUR_INVERT[force_colour] origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles', '0 0 0']) temp_data = conditions.import_template( temp_id, origin, angles, targetname=inst['targetname', ''], force_type=force_type, ) conditions.retexture_template( temp_data, origin, replace_tex, force_colour, force_grid, ) 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, angles) temp_data.detail['origin'] = br_origin # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't # modify it. vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail) # This is the original brush the template is replacing. We fix overlay # face IDs, so this brush is replaced by the faces in the template pointing # the same way. if replace_brush_pos is None: return pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z) pos += origin try: brush_group = SOLIDS[pos.as_tuple()] except KeyError: return vbsp.VMF.remove_brush(brush_group.solid) new_ids = [] all_brushes = temp_data.world # Overlays can't be applied to entities (other than func_detail). if temp_data.detail is not None and key_block is None: all_brushes.extend(temp_data.detail.solids) for brush in all_brushes: # type: VLib.Solid for face in brush.sides: # Only faces pointing the same way! if face.normal() == brush_group.normal: # Skip tool brushes (nodraw, player clips..) if face.mat.casefold().startswith('tools/'): continue new_ids.append(str(face.id)) if new_ids: conditions.reallocate_overlays({ str(brush_group.face.id): new_ids, })
def generate_fizzlers(vmf: VMF): """Generates fizzler models and the brushes according to their set types. After this is done, fizzler-related conditions will not function correctly. However the model instances are now available for modification. """ from vbsp import MAP_RAND_SEED for fizz in FIZZLERS.values(): if fizz.base_inst not in vmf.entities: continue # The fizzler was removed from the map. fizz_name = fizz.base_inst['targetname'] fizz_type = fizz.fizz_type # Static versions are only used for fizzlers which start on. # Permanently-off fizzlers are kinda useless, so we don't need # to bother optimising for it. is_static = bool( fizz.base_inst.fixup.int('$connectioncount', 0) == 0 and fizz.base_inst.fixup.bool('$start_enabled', 1)) pack_list = (fizz.fizz_type.pack_lists_static if is_static else fizz.fizz_type.pack_lists) for pack in pack_list: packing.pack_list(vmf, pack) if fizz_type.inst[FizzInst.BASE, is_static]: random.seed('{}_fizz_base_{}'.format(MAP_RAND_SEED, fizz_name)) fizz.base_inst['file'] = random.choice( fizz_type.inst[FizzInst.BASE, is_static]) if not fizz.emitters: LOGGER.warning('No emitters for fizzler "{}"!', fizz_name) continue # Brush index -> entity for ones that need to merge. # template_brush is used for the templated one. single_brushes = {} # type: Dict[FizzlerBrush, Entity] if fizz_type.temp_max or fizz_type.temp_min: template_brush_ent = vmf.create_ent( classname='func_brush', origin=fizz.base_inst['origin'], ) conditions.set_ent_keys( template_brush_ent, fizz.base_inst, fizz_type.temp_brush_keys, ) else: template_brush_ent = None up_dir = fizz.up_axis forward = (fizz.emitters[0][1] - fizz.emitters[0][0]).norm() min_angles = FIZZ_ANGLES[forward.as_tuple(), up_dir.as_tuple()] max_angles = FIZZ_ANGLES[(-forward).as_tuple(), up_dir.as_tuple()] model_min = (fizz_type.inst[FizzInst.PAIR_MIN, is_static] or fizz_type.inst[FizzInst.ALL, is_static]) model_max = (fizz_type.inst[FizzInst.PAIR_MAX, is_static] or fizz_type.inst[FizzInst.ALL, is_static]) if not model_min or not model_max: raise ValueError( 'No model specified for one side of "{}"' ' fizzlers'.format(fizz_type.id), ) # Define a function to do the model names. model_index = 0 if fizz_type.model_naming is ModelName.SAME: def get_model_name(ind): """Give every emitter the base's name.""" return fizz_name elif fizz_type.model_naming is ModelName.LOCAL: def get_model_name(ind): """Give every emitter a name local to the base.""" return fizz_name + '-' + fizz_type.model_name elif fizz_type.model_naming is ModelName.PAIRED: def get_model_name(ind): """Give each pair of emitters the same unique name.""" return '{}-{}{:02}'.format( fizz_name, fizz_type.model_name, ind, ) elif fizz_type.model_naming is ModelName.UNIQUE: def get_model_name(ind): """Give every model a unique name.""" nonlocal model_index model_index += 1 return '{}-{}{:02}'.format( fizz_name, fizz_type.model_name, model_index, ) else: raise ValueError('Bad ModelName?') # Generate env_beam pairs. for beam in fizz_type.beams: beam_template = Entity(vmf) conditions.set_ent_keys(beam_template, fizz.base_inst, beam.keys) beam_template['classname'] = 'env_beam' del beam_template[ 'LightningEnd'] # Don't allow users to set end pos. name = beam_template['targetname'] + '_' counter = 1 for seg_min, seg_max in fizz.emitters: for offset in beam.offset: # type: Vec min_off = offset.copy() max_off = offset.copy() min_off.localise(seg_min, min_angles) max_off.localise(seg_max, max_angles) beam_ent = beam_template.copy() vmf.add_ent(beam_ent) # Allow randomising speed and direction. if 0 < beam.speed_min < beam.speed_max: random.seed('{}{}{}'.format(MAP_RAND_SEED, min_off, max_off)) beam_ent['TextureScroll'] = random.randint( beam.speed_min, beam.speed_max) if random.choice((False, True)): # Flip to reverse direction. min_off, max_off = max_off, min_off beam_ent['origin'] = min_off beam_ent['LightningStart'] = beam_ent['targetname'] = ( name + str(counter)) counter += 1 beam_ent['targetpoint'] = max_off mat_mod_tex = {} # type: Dict[FizzlerBrush, Set[str]] for brush_type in fizz_type.brushes: if brush_type.mat_mod_var is not None: mat_mod_tex[brush_type] = set() # Record the data for trigger hurts so flinch triggers can match them. trigger_hurt_name = '' trigger_hurt_start_disabled = '0' for seg_ind, (seg_min, seg_max) in enumerate(fizz.emitters, start=1): length = (seg_max - seg_min).mag() random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_min)) if length == 128 and fizz_type.inst[FizzInst.PAIR_SINGLE, is_static]: min_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(fizz_type.inst[FizzInst.PAIR_SINGLE, is_static]), origin=(seg_min + seg_max) / 2, angles=min_angles, ) else: # Both side models. min_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(model_min), origin=seg_min, angles=min_angles, ) random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_max)) max_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(model_max), origin=seg_max, angles=max_angles, ) max_inst.fixup.update(fizz.base_inst.fixup) min_inst.fixup.update(fizz.base_inst.fixup) if fizz_type.inst[FizzInst.GRID, is_static]: # Generate one instance for each position. # Go 64 from each side, and always have at least 1 section # A 128 gap will have length = 0 for ind, dist in enumerate(range(64, round(length) - 63, 128)): mid_pos = seg_min + forward * dist random.seed('{}_fizz_mid_{}'.format( MAP_RAND_SEED, mid_pos)) mid_inst = vmf.create_ent( classname='func_instance', targetname=fizz_name, angles=min_angles, file=random.choice(fizz_type.inst[FizzInst.GRID, is_static]), origin=mid_pos, ) mid_inst.fixup.update(fizz.base_inst.fixup) if template_brush_ent is not None: if length == 128 and fizz_type.temp_single: temp = template_brush.import_template( fizz_type.temp_single, (seg_min + seg_max) / 2, min_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) else: if fizz_type.temp_min: temp = template_brush.import_template( fizz_type.temp_min, seg_min, min_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) if fizz_type.temp_max: temp = template_brush.import_template( fizz_type.temp_max, seg_max, max_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) # Generate the brushes. for brush_type in fizz_type.brushes: brush_ent = None # If singular, we reuse the same brush ent for all the segments. if brush_type.singular: brush_ent = single_brushes.get(brush_type, None) # Non-singular or not generated yet - make the entity. if brush_ent is None: brush_ent = vmf.create_ent(classname='func_brush') for key_name, key_value in brush_type.keys.items(): brush_ent[key_name] = conditions.resolve_value( fizz.base_inst, key_value) for key_name, key_value in brush_type.local_keys.items(): brush_ent[key_name] = conditions.local_name( fizz.base_inst, conditions.resolve_value( fizz.base_inst, key_value, )) brush_ent['targetname'] = conditions.local_name( fizz.base_inst, brush_type.name, ) # Set this to the center, to make sure it's not going to leak. brush_ent['origin'] = (seg_min + seg_max) / 2 # For fizzlers flat on the floor/ceiling, scanlines look # useless. Turn them off. if 'usescanline' in brush_ent and fizz.normal().z: brush_ent['UseScanline'] = 0 if brush_ent['classname'] == 'trigger_hurt': trigger_hurt_name = brush_ent['targetname'] trigger_hurt_start_disabled = brush_ent[ 'startdisabled'] if brush_type.set_axis_var: brush_ent['vscript_init_code'] = ( 'axis <- `{}`;'.format(fizz.normal().axis(), )) for out in brush_type.outputs: new_out = out.copy() new_out.target = conditions.local_name( fizz.base_inst, new_out.target, ) brush_ent.add_out(new_out) if brush_type.singular: # Record for the next iteration. single_brushes[brush_type] = brush_ent # If we have a material_modify_control to generate, # we need to parent it to ourselves to restrict it to us # only. We also need one for each material, so provide a # function to the generator which adds to a set. if brush_type.mat_mod_var is not None: used_tex_func = mat_mod_tex[brush_type].add else: def used_tex_func(val): """If not, ignore those calls.""" return None # Generate the brushes and texture them. brush_ent.solids.extend( brush_type.generate( vmf, fizz, seg_min, seg_max, used_tex_func, )) if trigger_hurt_name: fizz.gen_flinch_trigs( vmf, trigger_hurt_name, trigger_hurt_start_disabled, ) # If we have the config, but no templates used anywhere... if template_brush_ent is not None and not template_brush_ent.solids: template_brush_ent.remove() for brush_type, used_tex in mat_mod_tex.items(): brush_name = conditions.local_name(fizz.base_inst, brush_type.name) mat_mod_name = conditions.local_name(fizz.base_inst, brush_type.mat_mod_name) for off, tex in zip(MATMOD_OFFSETS, sorted(used_tex)): pos = off.copy().rotate(*min_angles) pos += Vec.from_str(fizz.base_inst['origin']) vmf.create_ent( classname='material_modify_control', origin=pos, targetname=mat_mod_name, materialName='materials/' + tex + '.vmt', materialVar=brush_type.mat_mod_var, parentname=brush_name, )
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_cust_fizzler(base_inst, res): """Customises the various components of a custom fizzler item. This should be executed on the base instance. Brush and MakeLaserField are ignored on laserfield barriers. Options: * ModelName: sets the targetname given to the model instances. * UniqueModel: If true, each model instance will get a suffix to allow unique targetnames. * Brush: A brush entity that will be generated (the original is deleted.) * Name is the instance name for the brush * Left/Right/Center/Short/Nodraw are the textures used * Keys are a block of keyvalues to be set. Targetname and Origin are auto-set. * Thickness will change the thickness of the fizzler if set. By default it is 2 units thick. * MakeLaserField generates a brush stretched across the whole area. * Name, 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 = utils.conv_bool(res['UniqueModel', '0']) 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 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 excecuted on LaserField!') return 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() 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'] = ( fizz_name + '-' + config['name', 'brush'] ) # All ents must have a classname! new_brush['classname'] = 'trigger_portal_cleanser' conditions.set_ent_keys( new_brush, base_inst, config, ) 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', 'effects/laserplane'] nodraw_tex = laserfield_conf['nodraw', 'tools/toolsnodraw'] tex_width = utils.conv_int( laserfield_conf['texwidth', '512'], 512 ) is_short = False for side in new_brush.sides(): if side.mat.casefold() == 'effects/fizzler': is_short = True break if is_short: for side in new_brush.sides(): if side.mat.casefold() == 'effects/fizzler': 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, ) else: # Just change the textures for side in new_brush.sides(): try: side.mat = config[ TEX_FIZZLER[side.mat.casefold()] ] except (KeyError, IndexError): # If we fail, just use the original textures pass widen_amount = utils.conv_float(config['thickness', '2'], 2.0) if widen_amount != 2: for brush in new_brush.solids: conditions.widen_fizz_brush( brush, thickness=widen_amount, )
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 edit_panel(vmf: VMF, inst: Entity, props: Property, create: bool) -> None: """Implements SetPanelOptions and CreatePanel.""" normal = props.vec('normal', 0, 0, 1).rotate_by_str(inst['angles']) origin = Vec.from_str(inst['origin']) uaxis, vaxis = Vec.INV_AXIS[normal.axis()] points: Set[Tuple[float, float, float]] = set() if 'point' in props: for prop in props.find_all('point'): points.add(conditions.resolve_offset(inst, prop.value, zoff=-64).as_tuple()) elif 'pos1' in props and 'pos2' in props: pos1, pos2 = Vec.bbox( conditions.resolve_offset(inst, props['pos1', '-48 -48 0'], zoff=-64), conditions.resolve_offset(inst, props['pos2', '48 48 0'], zoff=-64), ) points.update(map(Vec.as_tuple, Vec.iter_grid(pos1, pos2, 32))) else: # Default to the full tile. points.update({ (Vec(u, v, -64.0).rotate_by_str(inst['angles']) + origin).as_tuple() for u in [-48.0, -16.0, 16.0, 48.0] for v in [-48.0, -16.0, 16.0, 48.0] }) tiles_to_uv: Dict[tiling.TileDef, Set[Tuple[int, int]]] = defaultdict(set) for pos in points: try: tile, u, v = tiling.find_tile(Vec(pos), normal, force=create) except KeyError: continue tiles_to_uv[tile].add((u, v)) if not tiles_to_uv: LOGGER.warning('"{}": No tiles found for panels!', inst['targetname']) return # If bevels is provided, parse out the overall world positions. bevel_world: Optional[Set[Tuple[int, int]]] try: bevel_prop = props.find_key('bevel') except NoKeyError: bevel_world = None else: bevel_world = set() if bevel_prop.has_children(): # Individually specifying offsets. for bevel_str in bevel_prop.as_array(): bevel_point = Vec.from_str(bevel_str).rotate_by_str(inst['angles']) + origin bevel_world.add((int(bevel_point[uaxis]), int(bevel_point[vaxis]))) elif srctools.conv_bool(bevel_prop.value): # Fill the bounding box. bbox_min, bbox_max = Vec.bbox(map(Vec, points)) off = Vec.with_axes(uaxis, 32, vaxis, 32) bbox_min -= off bbox_max += off for pos in Vec.iter_grid(bbox_min, bbox_max, 32): if pos.as_tuple() not in points: bevel_world.add((pos[uaxis], pos[vaxis])) # else: No bevels. panels: List[tiling.Panel] = [] for tile, uvs in tiles_to_uv.items(): if create: panel = tiling.Panel( None, inst, tiling.PanelType.NORMAL, thickness=4, bevels=(), ) panel.points = uvs tile.panels.append(panel) else: for panel in tile.panels: if panel.same_item(inst) and panel.points == uvs: break else: LOGGER.warning( 'No panel to modify found for "{}"!', inst['targetname'] ) continue panels.append(panel) pan_type = '<nothing?>' try: pan_type = conditions.resolve_value(inst, props['type']) panel.pan_type = tiling.PanelType(pan_type.lower()) except LookupError: pass except ValueError: raise ValueError('Unknown panel type "{}"!'.format(pan_type)) if 'thickness' in props: panel.thickness = srctools.conv_int( conditions.resolve_value(inst, props['thickness']) ) if panel.thickness not in (2, 4, 8): raise ValueError( '"{}": Invalid panel thickess {}!\n' 'Must be 2, 4 or 8.', inst['targetname'], panel.thickness, ) if bevel_world is not None: panel.bevels.clear() for u, v in bevel_world: # Convert from world points to UV positions. u = (u - tile.pos[uaxis] + 48) / 32 v = (v - tile.pos[vaxis] + 48) / 32 # Cull outside here, we wont't use them. if -1 <= u <= 4 and -1 <= v <= 4: panel.bevels.add((u, v)) if 'offset' in props: panel.offset = conditions.resolve_offset(inst, props['offset']) panel.offset -= Vec.from_str(inst['origin']) if 'template' in props: # We only want the template inserted once. So remove it from all but one. if len(panels) == 1: panel.template = conditions.resolve_value(inst, props['template']) else: panel.template = '' if 'nodraw' in props: panel.nodraw = srctools.conv_bool( conditions.resolve_value(inst, props['nodraw']) ) if 'seal' in props: panel.seal = srctools.conv_bool( conditions.resolve_value(inst, props['seal']) ) if 'move_bullseye' in props: panel.steals_bullseye = srctools.conv_bool( conditions.resolve_value(inst, props['move_bullseye']) ) if 'keys' in props or 'localkeys' in props: # First grab the existing ent, so we can edit it. # These should all have the same value, unless they were independently # edited with mismatching point sets. In that case destroy all those existing ones. existing_ents: Set[Optional[Entity]] = {panel.brush_ent for panel in panels} try: [brush_ent] = existing_ents except ValueError: LOGGER.warning( 'Multiple independent panels for "{}" were made, then the ' 'brush entity was edited as a group! Discarding ' 'individual ents...', inst['targetname'] ) for brush_ent in existing_ents: if brush_ent is not None and brush_ent in vmf.entities: brush_ent.remove() brush_ent = None if brush_ent is None: brush_ent = vmf.create_ent('') old_pos = brush_ent.keys.pop('origin', None) conditions.set_ent_keys(brush_ent, inst, props) if not brush_ent['classname']: if create: # This doesn't make sense, you could just omit the prop. LOGGER.warning( 'No classname provided for panel "{}"!', inst['targetname'], ) # Make it a world brush. brush_ent.remove() brush_ent = None else: # We want to do some post-processing. # Localise any origin value. if 'origin' in brush_ent.keys: pos = Vec.from_str(brush_ent['origin']) pos.localise( Vec.from_str(inst['origin']), Vec.from_str(inst['angles']), ) brush_ent['origin'] = pos elif old_pos is not None: brush_ent['origin'] = old_pos # If it's func_detail, clear out all the keys. # Particularly `origin`, but the others are useless too. if brush_ent['classname'] == 'func_detail': brush_ent.clear_keys() brush_ent['classname'] = 'func_detail' for panel in panels: panel.brush_ent = brush_ent
def res_import_template(inst: Entity, 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`. - `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. - `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. """ ( orig_temp_id, replace_tex, force_colour, force_grid, force_type, surf_cat, bind_tile_pos, invert_var, color_var, visgroup_func, visgroup_force_var, visgroup_instvars, key_block, picker_vars, outputs, sense_offset, ) = res.value if ':' in orig_temp_id: # Split, resolve each part, then recombine. temp_id, visgroup = orig_temp_id.split(':', 1) temp_id = ( conditions.resolve_value(inst, temp_id) + ':' + conditions.resolve_value(inst, visgroup) ) else: temp_id = conditions.resolve_value(inst, orig_temp_id) if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)): def visgroup_func(group): """Use all the groups.""" yield from group # 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, inst) for flag in vis_flag_block): visgroups.add(vis_flag_block.real_name) if color_var.casefold() == '<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[force_colour] # else: False value, no invert. origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles', '0 0 0']) temp_data = template_brush.import_template( template, origin, angles, targetname=inst['targetname', ''], force_type=force_type, visgroup_choose=visgroup_func, add_to_map=True, additional_visgroups=visgroups, bind_tile_pos=bind_tile_pos, ) 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, angles) 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).rotate(*angles) temp_data.detail['movedir'] = move_dir.to_angle() for out in outputs: # type: Output 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, ) # type: Optional[texturing.Portalable] if picker_val is not None: inst.fixup[picker_var] = picker_val.value else: inst.fixup[picker_var] = ''
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 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'] axis = Vec.with_axes(des_axis, 1).rotate_by_str(ent['angles', '0 0 0']) 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 try: flag_values = FLAG_ROTATING[door_type] except KeyError: LOGGER.warning('Unknown rotating brush type "{}"!', door_type) return name = res['ModifyTarget', ''] if name: name = conditions.local_name(ent, name) setter_loc = ent['origin'] else: # Generate a brush. name = conditions.local_name(ent, res['name', '']) pos = res.vec('Pos').rotate_by_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 = [vbsp.VMF.make_prism(pos - 1, pos + 1).solid] # Add or remove flags as needed by creating KV setters. for flag, value in zip( ('x', 'y', 'z', 'rev'), [axis.x != 0, axis.y != 0, axis.z != 0, reverse], ): if flag in flag_values: vmf.create_ent( 'comp_kv_setter', origin=setter_loc, target=name, mode='flags', kv_name=flag_values[flag], kv_value_local=value, ) # 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_local='1' if reverse else '-1', )
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 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_fix_rotation_axis(ent: Entity, res: Property): """Generate a `func_rotating`, `func_door_rotating` or any similar entity. This uses the orientation of the instance to detemine the correct spawnflags to make it rotate in the correct direction. The brush will be 2x2x2 units large, and always set to be non-solid. - `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`, `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_rotating` * `func_door_rotating` * `func_rot_button` * `func_platrot` """ des_axis = res['axis', 'z'].casefold() reverse = srctools.conv_bool(res['reversed', '0']) door_type = res['classname', 'func_door_rotating'] # Extra stuff to apply to the flags (USE, toggle, etc) flags = sum( map( # Add together multiple values srctools.conv_int, res['flags', '0'].split('+'))) name = conditions.local_name(ent, res['name', '']) axis = Vec(**{des_axis: 1}).rotate_by_str(ent['angles', '0 0 0']) pos = Vec.from_str(res['Pos', '0 0 0']).rotate_by_str(ent['angles', '0 0 0']) pos += Vec.from_str(ent['origin', '0 0 0']) door_ent = vbsp.VMF.create_ent( classname=door_type, targetname=name, origin=pos.join(' '), ) 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 = [vbsp.VMF.make_prism(pos - 1, pos + 1).solid] 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 flag_values = FLAG_ROTATING[door_type] # Make the door always non-solid! flags |= flag_values.get('solid_flags', 0) # Add or remove flags as needed. # flags |= bit sets it to 1. # flags |= ~bit sets it to 0. if axis.x != 0: flags |= flag_values.get('x', 0) else: flags &= ~flag_values.get('x', 0) if axis.y != 0: flags |= flag_values.get('y', 0) else: flags &= ~flag_values.get('y', 0) if axis.z != 0: flags |= flag_values.get('z', 0) else: flags &= ~flag_values.get('z', 0) if door_type == 'momentary_rot_button': door_ent['startdirection'] = '1' if reverse else '-1' else: if reverse: flags |= flag_values.get('rev', 0) else: flags &= ~flag_values.get('rev', 0) door_ent['spawnflags'] = str(flags)
def generate_fizzlers(vmf: VMF): """Generates fizzler models and the brushes according to their set types. After this is done, fizzler-related conditions will not function correctly. However the model instances are now available for modification. """ from vbsp import MAP_RAND_SEED for fizz in FIZZLERS.values(): if fizz.base_inst not in vmf.entities: continue # The fizzler was removed from the map. fizz_name = fizz.base_inst['targetname'] fizz_type = fizz.fizz_type # Static versions are only used for fizzlers which start on. # Permanently-off fizzlers are kinda useless, so we don't need # to bother optimising for it. is_static = bool( fizz.base_inst.fixup.int('$connectioncount', 0) == 0 and fizz.base_inst.fixup.bool('$start_enabled', 1) ) pack_list = ( fizz.fizz_type.pack_lists_static if is_static else fizz.fizz_type.pack_lists ) for pack in pack_list: packing.pack_list(vmf, pack) if fizz_type.inst[FizzInst.BASE, is_static]: random.seed('{}_fizz_base_{}'.format(MAP_RAND_SEED, fizz_name)) fizz.base_inst['file'] = random.choice(fizz_type.inst[FizzInst.BASE, is_static]) if not fizz.emitters: LOGGER.warning('No emitters for fizzler "{}"!', fizz_name) continue # Brush index -> entity for ones that need to merge. # template_brush is used for the templated one. single_brushes = {} # type: Dict[FizzlerBrush, Entity] if fizz_type.temp_max or fizz_type.temp_min: template_brush_ent = vmf.create_ent( classname='func_brush', origin=fizz.base_inst['origin'], ) conditions.set_ent_keys( template_brush_ent, fizz.base_inst, fizz_type.temp_brush_keys, ) else: template_brush_ent = None up_dir = fizz.up_axis forward = (fizz.emitters[0][1] - fizz.emitters[0][0]).norm() min_angles = FIZZ_ANGLES[forward.as_tuple(), up_dir.as_tuple()] max_angles = FIZZ_ANGLES[(-forward).as_tuple(), up_dir.as_tuple()] model_min = ( fizz_type.inst[FizzInst.PAIR_MIN, is_static] or fizz_type.inst[FizzInst.ALL, is_static] ) model_max = ( fizz_type.inst[FizzInst.PAIR_MAX, is_static] or fizz_type.inst[FizzInst.ALL, is_static] ) if not model_min or not model_max: raise ValueError( 'No model specified for one side of "{}"' ' fizzlers'.format(fizz_type.id), ) # Define a function to do the model names. model_index = 0 if fizz_type.model_naming is ModelName.SAME: def get_model_name(ind): """Give every emitter the base's name.""" return fizz_name elif fizz_type.model_naming is ModelName.LOCAL: def get_model_name(ind): """Give every emitter a name local to the base.""" return fizz_name + '-' + fizz_type.model_name elif fizz_type.model_naming is ModelName.PAIRED: def get_model_name(ind): """Give each pair of emitters the same unique name.""" return '{}-{}{:02}'.format( fizz_name, fizz_type.model_name, ind, ) elif fizz_type.model_naming is ModelName.UNIQUE: def get_model_name(ind): """Give every model a unique name.""" nonlocal model_index model_index += 1 return '{}-{}{:02}'.format( fizz_name, fizz_type.model_name, model_index, ) else: raise ValueError('Bad ModelName?') # Generate env_beam pairs. for beam in fizz_type.beams: beam_template = Entity(vmf) conditions.set_ent_keys(beam_template, fizz.base_inst, beam.keys) beam_template['classname'] = 'env_beam' del beam_template['LightningEnd'] # Don't allow users to set end pos. name = beam_template['targetname'] + '_' counter = 1 for seg_min, seg_max in fizz.emitters: for offset in beam.offset: # type: Vec min_off = offset.copy() max_off = offset.copy() min_off.localise(seg_min, min_angles) max_off.localise(seg_max, max_angles) beam_ent = beam_template.copy() vmf.add_ent(beam_ent) # Allow randomising speed and direction. if 0 < beam.speed_min < beam.speed_max: random.seed('{}{}{}'.format(MAP_RAND_SEED, min_off, max_off)) beam_ent['TextureScroll'] = random.randint(beam.speed_min, beam.speed_max) if random.choice((False, True)): # Flip to reverse direction. min_off, max_off = max_off, min_off beam_ent['origin'] = min_off beam_ent['LightningStart'] = beam_ent['targetname'] = ( name + str(counter) ) counter += 1 beam_ent['targetpoint'] = max_off # Prepare to copy over instance traits for the emitters. fizz_traits = instance_traits.get(fizz.base_inst).copy() # Special case, mark emitters that have a custom position for Clean # models. if fizz.has_cust_position: fizz_traits.add('cust_shape') mat_mod_tex = {} # type: Dict[FizzlerBrush, Set[str]] for brush_type in fizz_type.brushes: if brush_type.mat_mod_var is not None: mat_mod_tex[brush_type] = set() # Record the data for trigger hurts so flinch triggers can match them. trigger_hurt_name = '' trigger_hurt_start_disabled = '0' for seg_ind, (seg_min, seg_max) in enumerate(fizz.emitters, start=1): length = (seg_max - seg_min).mag() random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_min)) if length == 128 and fizz_type.inst[FizzInst.PAIR_SINGLE, is_static]: min_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(fizz_type.inst[FizzInst.PAIR_SINGLE, is_static]), origin=(seg_min + seg_max)/2, angles=min_angles, ) else: # Both side models. min_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(model_min), origin=seg_min, angles=min_angles, ) random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_max)) max_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(model_max), origin=seg_max, angles=max_angles, ) max_inst.fixup.update(fizz.base_inst.fixup) instance_traits.get(max_inst).update(fizz_traits) min_inst.fixup.update(fizz.base_inst.fixup) instance_traits.get(min_inst).update(fizz_traits) if fizz_type.inst[FizzInst.GRID, is_static]: # Generate one instance for each position. # Go 64 from each side, and always have at least 1 section # A 128 gap will have length = 0 for ind, dist in enumerate(range(64, round(length) - 63, 128)): mid_pos = seg_min + forward * dist random.seed('{}_fizz_mid_{}'.format(MAP_RAND_SEED, mid_pos)) mid_inst = vmf.create_ent( classname='func_instance', targetname=fizz_name, angles=min_angles, file=random.choice(fizz_type.inst[FizzInst.GRID, is_static]), origin=mid_pos, ) mid_inst.fixup.update(fizz.base_inst.fixup) instance_traits.get(mid_inst).update(fizz_traits) if template_brush_ent is not None: if length == 128 and fizz_type.temp_single: temp = template_brush.import_template( fizz_type.temp_single, (seg_min + seg_max) / 2, min_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) else: if fizz_type.temp_min: temp = template_brush.import_template( fizz_type.temp_min, seg_min, min_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) if fizz_type.temp_max: temp = template_brush.import_template( fizz_type.temp_max, seg_max, max_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) # Generate the brushes. for brush_type in fizz_type.brushes: brush_ent = None # If singular, we reuse the same brush ent for all the segments. if brush_type.singular: brush_ent = single_brushes.get(brush_type, None) # Non-singular or not generated yet - make the entity. if brush_ent is None: brush_ent = vmf.create_ent(classname='func_brush') for key_name, key_value in brush_type.keys.items(): brush_ent[key_name] = conditions.resolve_value(fizz.base_inst, key_value) for key_name, key_value in brush_type.local_keys.items(): brush_ent[key_name] = conditions.local_name( fizz.base_inst, conditions.resolve_value( fizz.base_inst, key_value, ) ) brush_ent['targetname'] = conditions.local_name( fizz.base_inst, brush_type.name, ) # Set this to the center, to make sure it's not going to leak. brush_ent['origin'] = (seg_min + seg_max)/2 # For fizzlers flat on the floor/ceiling, scanlines look # useless. Turn them off. if 'usescanline' in brush_ent and fizz.normal().z: brush_ent['UseScanline'] = 0 if brush_ent['classname'] == 'trigger_hurt': trigger_hurt_name = brush_ent['targetname'] trigger_hurt_start_disabled = brush_ent['startdisabled'] if brush_type.set_axis_var: brush_ent['vscript_init_code'] = ( 'axis <- `{}`;'.format( fizz.normal().axis(), ) ) for out in brush_type.outputs: new_out = out.copy() new_out.target = conditions.local_name( fizz.base_inst, new_out.target, ) brush_ent.add_out(new_out) if brush_type.singular: # Record for the next iteration. single_brushes[brush_type] = brush_ent # If we have a material_modify_control to generate, # we need to parent it to ourselves to restrict it to us # only. We also need one for each material, so provide a # function to the generator which adds to a set. if brush_type.mat_mod_var is not None: used_tex_func = mat_mod_tex[brush_type].add else: def used_tex_func(val): """If not, ignore those calls.""" return None # Generate the brushes and texture them. brush_ent.solids.extend( brush_type.generate( vmf, fizz, seg_min, seg_max, used_tex_func, ) ) if trigger_hurt_name: fizz.gen_flinch_trigs( vmf, trigger_hurt_name, trigger_hurt_start_disabled, ) # If we have the config, but no templates used anywhere... if template_brush_ent is not None and not template_brush_ent.solids: template_brush_ent.remove() for brush_type, used_tex in mat_mod_tex.items(): brush_name = conditions.local_name(fizz.base_inst, brush_type.name) mat_mod_name = conditions.local_name(fizz.base_inst, brush_type.mat_mod_name) for off, tex in zip(MATMOD_OFFSETS, sorted(used_tex)): pos = off.copy().rotate(*min_angles) pos += Vec.from_str(fizz.base_inst['origin']) vmf.create_ent( classname='material_modify_control', origin=pos, targetname=mat_mod_name, materialName='materials/' + tex + '.vmt', materialVar=brush_type.mat_mod_var, parentname=brush_name, )