def sceneset(ctx: Context): """Chains a set of choreographed scenes together.""" for ent in ctx.vmf.by_class['comp_choreo_sceneset']: scenes = [ ent['scene{:02}'.format(i)] for i in range(1, 21) if ent['scene{:02}'.format(i)] ] if not scenes: LOGGER.warning( '"{}" at ({}) has no scenes!', ent['targetname'], ent['origin'], ) continue if conv_bool(ent['play_dings']): scenes.insert(0, 'scenes/npc/glados_manual/ding_on.vcd') scenes.append('scenes/npc/glados_manual/ding_off.vcd') delay = conv_float(ent['delay'], 0.1) only_once = conv_bool(ent['only_once']) ent.remove() start_ent = None name = ent['targetname'] or '_choreo_{}'.format(ent.id) for i, scene in enumerate(scenes): part = ctx.vmf.create_ent( classname='logic_choreographed_scene', targetname=('{}_{}'.format(name, i) if i > 0 else name), origin=ent['origin'], scenefile=scene, ) if i + 1 < len(scenes): part.add_out( Output( 'OnCompletion', '{}_{}'.format(name, i + 1), 'Start', delay=delay, )) if only_once: # When started blank the name so it can't be triggered, # then clean up after finished part.add_out( Output('OnStart', '!self', 'AddOutput', 'targetname '), Output('OnCompletion', '!self', 'Kill'), ) if start_ent is None: start_ent = part assert start_ent is not None, "Has scenes but none made?" for out in ent.outputs: if out.output.casefold() == 'onstart': start_ent.add_out(out) elif out.output.casefold() == 'onfinish': # Part is the last in the loop. out.output = 'OnCompletion' part.add_out(out)
def flag_brush_at_loc(inst: Entity, flag: Property): """Checks to see if a wall is present at the given location. - Pos is the position of the brush, where `0 0 0` is the floor-position of the brush. - Dir is the normal the face is pointing. (0 0 -1) is 'up'. - Type defines the type the brush must be: - "Any" requires either a black or white brush. - "None" means that no brush must be present. - "White" requires a portalable surface. - "Black" requires a non-portalable surface. - SetVar defines an instvar which will be given a value of "black", "white" or "none" to allow the result to be reused. - If gridPos is true, the position will be snapped so it aligns with the 128 brushes (Useful with fizzler/light strip items). - RemoveBrush: If set to 1, the brush will be removed if found. Only do this to EmbedFace brushes, since it will remove the other sides as well. """ from conditions import VMF pos = Vec.from_str(flag['pos', '0 0 0']) pos.z -= 64 # Subtract so origin is the floor-position pos = pos.rotate_by_str(inst['angles', '0 0 0']) # Relative to the instance origin pos += Vec.from_str(inst['origin', '0 0 0']) norm = flag['dir', None] if norm is not None: norm = Vec.from_str(norm).rotate_by_str(inst['angles', '0 0 0'], ) if srctools.conv_bool(flag['gridpos', '0']) and norm is not None: for axis in 'xyz': # Don't realign things in the normal's axis - # those are already fine. if norm[axis] == 0: pos[axis] = pos[axis] // 128 * 128 + 64 result_var = flag['setVar', ''] should_remove = srctools.conv_bool(flag['RemoveBrush', False], False) des_type = flag['type', 'any'].casefold() brush = SOLIDS.get(pos.as_tuple(), None) if brush is None or (norm is not None and abs(brush.normal) != abs(norm)): br_type = 'none' else: br_type = str(brush.color) if should_remove: VMF.remove_brush(brush.solid, ) if result_var: inst.fixup[result_var] = br_type if des_type == 'any' and br_type != 'none': return True return des_type == br_type
def res_unst_scaffold_setup(res: Property): group = res['group', 'DEFAULT_GROUP'] if group not in SCAFFOLD_CONFIGS: # Store our values in the CONFIGS dictionary targ_inst, links = SCAFFOLD_CONFIGS[group] = {}, {} else: # Grab the already-filled values, and add to them targ_inst, links = SCAFFOLD_CONFIGS[group] for block in res.find_all("Instance"): conf = { # If set, adjusts the offset appropriately 'is_piston': srctools.conv_bool(block['isPiston', '0']), 'rotate_logic': srctools.conv_bool(block['AlterAng', '1'], True), 'off_floor': Vec.from_str(block['FloorOff', '0 0 0']), 'off_wall': Vec.from_str(block['WallOff', '0 0 0']), 'logic_start': block['startlogic', ''], 'logic_end': block['endLogic', ''], 'logic_mid': block['midLogic', ''], 'logic_start_rev': block['StartLogicRev', None], 'logic_end_rev': block['EndLogicRev', None], 'logic_mid_rev': block['EndLogicRev', None], 'inst_wall': block['wallInst', ''], 'inst_floor': block['floorInst', ''], 'inst_offset': block['offsetInst', None], # Specially rotated to face the next track! 'inst_end': block['endInst', None], } for logic_type in ('logic_start', 'logic_mid', 'logic_end'): if conf[logic_type + '_rev'] is None: conf[logic_type + '_rev'] = conf[logic_type] for inst in instanceLocs.resolve(block['file']): targ_inst[inst] = conf # We need to provide vars to link the tracks and beams. for block in res.find_all('LinkEnt'): # The name for this set of entities. # It must be a '@' name, or the name will be fixed-up incorrectly! loc_name = block['name'] if not loc_name.startswith('@'): loc_name = '@' + loc_name links[block['nameVar']] = { 'name': loc_name, # The next entity (not set in end logic) 'next': block['nextVar'], # A '*' name to reference all the ents (set on the start logic) 'all': block['allVar', None], } return group # We look up the group name to find the values.
def res_unst_scaffold_setup(res: Property): group = res["group", "DEFAULT_GROUP"] if group not in SCAFFOLD_CONFIGS: # Store our values in the CONFIGS dictionary targ_inst, links = SCAFFOLD_CONFIGS[group] = {}, {} else: # Grab the already-filled values, and add to them targ_inst, links = SCAFFOLD_CONFIGS[group] for block in res.find_all("Instance"): conf = { # If set, adjusts the offset appropriately "is_piston": srctools.conv_bool(block["isPiston", "0"]), "rotate_logic": srctools.conv_bool(block["AlterAng", "1"], True), "off_floor": Vec.from_str(block["FloorOff", "0 0 0"]), "off_wall": Vec.from_str(block["WallOff", "0 0 0"]), "logic_start": block["startlogic", ""], "logic_end": block["endLogic", ""], "logic_mid": block["midLogic", ""], "logic_start_rev": block["StartLogicRev", None], "logic_end_rev": block["EndLogicRev", None], "logic_mid_rev": block["EndLogicRev", None], "inst_wall": block["wallInst", ""], "inst_floor": block["floorInst", ""], "inst_offset": block["offsetInst", None], # Specially rotated to face the next track! "inst_end": block["endInst", None], } for logic_type in ("logic_start", "logic_mid", "logic_end"): if conf[logic_type + "_rev"] is None: conf[logic_type + "_rev"] = conf[logic_type] for inst in resolve_inst(block["file"]): targ_inst[inst] = conf # We need to provide vars to link the tracks and beams. for block in res.find_all("LinkEnt"): # The name for this set of entities. # It must be a '@' name, or the name will be fixed-up incorrectly! loc_name = block["name"] if not loc_name.startswith("@"): loc_name = "@" + loc_name links[block["nameVar"]] = { "name": loc_name, # The next entity (not set in end logic) "next": block["nextVar"], # A '*' name to reference all the ents (set on the start logic) "all": block["allVar", None], } return group # We look up the group name to find the values.
def test_conv_bool(): """Test srctools.conv_bool()""" for val in true_strings: assert srctools.conv_bool(val) for val in false_strings: assert not srctools.conv_bool(val) # Check that bools pass through assert srctools.conv_bool(True) assert not srctools.conv_bool(False) # None passes through the default for val in def_vals: assert srctools.conv_bool(None, val) is val
def do_item_optimisation(vmf: VMF): """Optimise redundant logic items.""" needs_global_toggle = False for item in list(ITEMS.values()): # We can't remove items that have functionality, or don't have IO. if item.item_type is None or not item.item_type.input_type.is_logic: continue prim_inverted = conv_bool( conditions.resolve_value( item.inst, item.item_type.invert_var, )) sec_inverted = conv_bool( conditions.resolve_value( item.inst, item.item_type.sec_invert_var, )) # Don't optimise if inverted. if prim_inverted or sec_inverted: continue inp_count = len(item.inputs) if inp_count == 0: # Totally useless, remove. # We just leave the panel entities, and tie all the antlines # to the same toggle. needs_global_toggle = True for ent in item.antlines: ent['targetname'] = '_static_ind' del ITEMS[item.name] item.inst.remove() elif inp_count == 1: # Only one input, so AND or OR are useless. # Transfer input item to point to the output(s). collapse_item(item) # The antlines need a toggle entity, otherwise they'll copy random other # overlays. if needs_global_toggle: vmf.create_ent( classname='env_texturetoggle', origin=vbsp_options.get(Vec, 'global_ents_loc'), targetname='_static_ind_tog', target='_static_ind', )
def res_rand_vec(inst: Entity, res: Property) -> None: """A modification to RandomNum which generates a random vector instead. 'decimal', 'seed' and 'ResultVar' work like RandomNum. min/max x/y/z are for each section. If the min and max are equal that number will be used instead. """ is_float = srctools.conv_bool(res['decimal']) var = res['resultvar', '$random'] set_random_seed(inst, 'e' + res['seed', 'random']) if is_float: func = random.uniform else: func = random.randint value = Vec() for axis in 'xyz': max_val = srctools.conv_float(res['max_' + axis, 0.0]) min_val = srctools.conv_float(res['min_' + axis, 0.0]) if min_val == max_val: value[axis] = min_val else: value[axis] = func(min_val, max_val) inst.fixup[var] = value.join(' ')
def res_add_output_setup(res: Property): output = res['output'] input_name = res['input'] inst_in = res['inst_in', ''] inst_out = res['inst_out', ''] targ = res['target'] only_once = srctools.conv_bool(res['only_once', None]) times = 1 if only_once else srctools.conv_int(res['times', None], -1) delay = res['delay', '0.0'] parm = res['parm', ''] if output.startswith('<') and output.endswith('>'): out_id, out_type = output.strip('<>').split(':', 1) out_id = out_id.casefold() out_type = out_type.strip().casefold() else: out_id, out_type = output, 'const' return ( out_type, out_id, targ, input_name, parm, delay, times, inst_in, inst_out, )
def load_settings(pit: Property): if not pit: SETTINGS.clear() # No pits are permitted.. return SETTINGS.update({ 'use_skybox': srctools.conv_bool(pit['teleport', '0']), 'tele_dest': pit['tele_target', '@goo_targ'], 'tele_ref': pit['tele_ref', '@goo_ref'], 'off_x': srctools.conv_int(pit['off_x', '0']), 'off_y': srctools.conv_int(pit['off_y', '0']), 'skybox': pit['sky_inst', ''], 'skybox_ceil': pit['sky_inst_ceil', ''], 'targ': pit['targ_inst', ''], 'blend_light': pit['blend_light', ''] }) for inst_type in ( 'support', 'side', 'corner', 'double', 'triple', 'pillar', ): vals = [ prop.value for prop in pit.find_all(inst_type + '_inst') ] if len(vals) == 0: vals = [""] PIT_INST[inst_type] = vals
def res_rand_vec(inst: Entity, res: Property): """A modification to RandomNum which generates a random vector instead. 'decimal', 'seed' and 'ResultVar' work like RandomNum. min/max x/y/z are for each section. If the min and max are equal that number will be used instead. """ is_float = srctools.conv_bool(res['decimal']) var = res['resultvar', '$random'] seed = res['seed', 'random'] random.seed(inst['origin'] + inst['angles'] + 'random_' + seed) if is_float: func = random.uniform else: func = random.randint value = Vec() for axis in 'xyz': max_val = srctools.conv_float(res['max_' + axis, 0.0]) min_val = srctools.conv_float(res['min_' + axis, 0.0]) if min_val == max_val: value[axis] = min_val else: value[axis] = func(min_val, max_val) inst.fixup[var] = value.join(' ')
def generate_resp_script(file, allow_dings): """Write the responses section into a file.""" use_dings = allow_dings config = ConfigFile('resp_voice.cfg', root='bee2') file.write("BEE2_RESPONSES <- {\n") for section in QUOTE_DATA.find_key('CoopResponses', []): if not section.has_children() and section.name == 'use_dings': # Allow overriding specifically for the response script use_dings = srctools.conv_bool(section.value, allow_dings) continue voice_attr = RESP_HAS_NAMES.get(section.name, '') if voice_attr and not map_attr[voice_attr]: continue # This response catagory isn't present section_data = ['\t{} = [\n'.format(section.name)] for index, line in enumerate(section): if not config.getboolean(section.name, "line_" + str(index), True): # It's disabled! continue section_data.append('\t\tCreateSceneEntity("{}"),\n'.format( line['choreo'])) if len(section_data) != 1: for line in section_data: file.write(line) file.write('\t],\n') file.write('}\n') file.write( 'BEE2_PLAY_DING = {};\n'.format('true' if use_dings else 'false'))
def res_rand_vec(inst: Entity, res: Property) -> None: """A modification to RandomNum which generates a random vector instead. `decimal`, `seed` and `ResultVar` work like RandomNum. `min_x`, `max_y` etc are used to define the boundaries. If the min and max are equal that number will be always used instead. """ is_float = srctools.conv_bool(res['decimal']) var = res['resultvar', '$random'] set_random_seed(inst, 'e' + res['seed', 'random']) if is_float: func = random.uniform else: func = random.randint value = Vec() for axis in 'xyz': max_val = srctools.conv_float(res['max_' + axis, 0.0]) min_val = srctools.conv_float(res['min_' + axis, 0.0]) if min_val == max_val: value[axis] = min_val else: value[axis] = func(min_val, max_val) inst.fixup[var] = value.join(' ')
def from_ent(cls, ent: Entity) -> Iterator[BBox]: """Parse keyvalues on a VMF entity. One bounding box is produced for each brush.""" coll = CollideType.NOTHING for key, value in ent.keys.items(): if key.casefold().startswith('coll_') and conv_bool(value): coll_name = key[5:].upper() try: coll |= CollideType[coll_name] except KeyError: LOGGER.warning('Invalid collide type: "{}"!', key) tags = frozenset(ent['tags'].split()) for solid in ent.solids: mins, maxes = solid.get_bbox() non_skip_faces = [ face for face in solid if face.mat != consts.Tools.SKIP ] try: # Only one non-skip face, "flatten" along its plane. face: Side [face] = non_skip_faces except ValueError: pass # Regular bbox. else: plane_norm = face.normal() plane_point = face.planes[0] for point in [mins, maxes]: # Get the offset from the plane, then subtract to force it onto the plane. point -= plane_norm * Vec.dot(point - plane_point, plane_norm) yield cls(mins, maxes, contents=coll, tags=tags)
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 acess 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 set_opt(self, opt_name: str, value: str) -> None: """Set an option to a specific value.""" folded_name = opt_name.casefold() for opt in self.defaults: if folded_name == opt.id: break else: LOGGER.warning('Invalid option name "{}"!', opt_name) return if opt.type is TYPE.RAW: if not isinstance(value, Property): raise ValueError('The value must be a Property ' 'for property blocks!') self.settings[opt.id] = value elif opt.type is TYPE.VEC: # Pass nones so we can check if it failed.. parsed_vals = parse_vec_str(value, x=None) if parsed_vals[0] is None: return self.settings[opt.id] = Vec(*parsed_vals) elif opt.type is TYPE.BOOL: self.settings[opt.id] = conv_bool(value, self.settings[opt.id]) else: # int, float, str - no special handling... try: self.settings[opt.id] = opt.type.convert(value) except (ValueError, TypeError): pass
def laser_catcher_skins(ctx: Context): """Fix Valve's bug where reloading saves causes lasers to get their skin wrong.""" for ent in ctx.vmf.by_class['prop_laser_catcher']: if not conv_bool(ent['src_fix_skins'], True): continue deact_skin, act_skin = '23' if ent['SkinType'] == '1' else '01' # Look for outputs which do this already. name = ent['targetname'] has_act = has_deact = False for out in ent.outputs: if has_act and has_deact: break if out.target == name or out.target == '!self': if out.input.casefold() == 'skin': if out.params == act_skin: has_act = True elif out.params == act_skin: has_act = True if not has_act: ent.add_out(Output('OnPowered', '!self', 'Skin', act_skin)) if not has_deact: ent.add_out(Output('OnUnPowered', '!self', 'Skin', deact_skin))
def res_vactube_setup(res: Property): group = res['group', 'DEFAULT_GROUP'] if group not in VAC_CONFIGS: # Store our values in the CONFIGS dictionary config, inst_configs = VAC_CONFIGS[group] = {}, {} else: # Grab the already-filled values, and add to them config, inst_configs = VAC_CONFIGS[group] for block in res.find_all("Instance"): # Configuration info for each instance set.. conf = { # The three sizes of corner instance ('corner', 1): block['corner_small_inst', ''], ('corner', 2): block['corner_medium_inst', ''], ('corner', 3): block['corner_large_inst', ''], ('corner_temp', 1): block['temp_corner_small', ''], ('corner_temp', 2): block['temp_corner_medium', ''], ('corner_temp', 3): block['temp_corner_large', ''], # Straight instances connected to the next part 'straight': block['straight_inst', ''], # Supports attach to the 4 sides of the straight part, # if there's a brush there. 'support': block['support_inst', ''], 'is_tsection': srctools.conv_bool(block['is_tsection', '0']), ('entry', 'wall'): block['entry_inst'], ('entry', 'floor'): block['entry_floor_inst'], ('entry', 'ceiling'): block['entry_ceil_inst'], 'exit': block['exit_inst'], } for prop in block.find_all("File"): try: size, file = prop.value.split(":", 1) except ValueError: size = 1 file = prop.value for inst in instanceLocs.resolve(file): inst_configs[inst] = conf, srctools.conv_int(size, 1) return group
def load(opt_blocks: Iterator[Property]): """Read settings from the given property block.""" SETTINGS.clear() set_vals = {} for opt_block in opt_blocks: for prop in opt_block: set_vals[prop.name] = prop.value options = {opt.id: opt for opt in DEFAULTS} if len(options) != len(DEFAULTS): from collections import Counter # Find ids used more than once.. raise Exception( "Duplicate option(s)! ({})".format( ", ".join(k for k, v in Counter(opt.id for opt in DEFAULTS).items() if v > 1) ) ) fallback_opts = [] for opt in DEFAULTS: try: val = set_vals.pop(opt.id) except KeyError: if opt.fallback is not None: fallback_opts.append(opt) assert opt.fallback in options, "Invalid fallback in " + opt.id else: SETTINGS[opt.id] = opt.default continue if opt.type is TYPE.VEC: # Pass nones so we can check if it failed.. parsed_vals = parse_vec_str(val, x=None) if parsed_vals[0] is None: SETTINGS[opt.id] = opt.default else: SETTINGS[opt.id] = Vec(*parsed_vals) elif opt.type is TYPE.BOOL: SETTINGS[opt.id] = srctools.conv_bool(val, opt.default) else: # int, float, str - no special handling... try: SETTINGS[opt.id] = opt.type.value(val) except (ValueError, TypeError): SETTINGS[opt.id] = opt.default for opt in fallback_opts: try: SETTINGS[opt.id] = SETTINGS[opt.fallback] except KeyError: raise Exception('Bad fallback for "{}"!'.format(opt.id)) # Check they have the same type. assert opt.type is options[opt.fallback].type if set_vals: LOGGER.warning("Extra config options: {}", set_vals)
def res_water_splash_setup(res: Property): parent = res['parent'] name = res['name'] scale = srctools.conv_float(res['scale', ''], 8.0) pos1 = Vec.from_str(res['position', '']) calc_type = res['type', ''] pos2 = res['position2', ''] fast_check = srctools.conv_bool(res['fast_check', '']) return name, parent, scale, pos1, pos2, calc_type, fast_check
def flag_is_preview(flag: Property) -> bool: """Checks if the preview mode status equals the given value. If preview mode is enabled, the player will start before the entry door, and restart the map after reaching the exit door. If `False`, they start in the elevator. Preview mode is always `False` when publishing. """ return vbsp.IS_PREVIEW == conv_bool(flag.value, False)
def flag_is_preview(flag: Property) -> bool: """Checks if the preview mode status equals the given value. If preview mode is enabled, the player will start before the entry door, and restart the map after reaching the exit door. If `False`, they start in the elevator. Preview mode is always `False` when publishing. """ return global_bool(vbsp.IS_PREVIEW == conv_bool(flag.value, False))
def comp_relay(ctx: Context): """Implements comp_relay, allowing zero-overhead relay ents for managing outputs. These are collapsed into their callers. """ # Output -> input that we convert. out_names = { 'ontrigger': 'trigger', 'onturnedon': 'turnon', 'onturnedoff': 'turnoff', } # Add user outputs as well. for i in '12345678': out_names['onuser' + i] = 'fireuser' + i for relay in ctx.vmf.by_class['comp_relay']: # First, see if any entities exist with the same name that aren't # comp_relays. In that case, we need to keep the inputs. relay_name = relay['targetname'] should_remove = not any(ent['classname'].casefold() != 'comp_relay' for ent in ctx.vmf.by_target[relay_name]) # If ctrl_type is 0, ctrl_value needs to be 1 to be enabled. # If ctrl_type is 1, ctrl_value needs to be 0 to be enabled. enabled = conv_bool(relay['ctrl_type']) != conv_bool( relay['ctrl_value']) for out in relay.outputs: try: inp_name = out_names[out.output.casefold()] except KeyError: LOGGER.warning( 'Unknown output "{}" on comp_relay "{}"!\n' 'This will be discarded.', out.output, relay_name, ) continue if enabled: out.output = inp_name ctx.add_io_remap(relay_name, out, remove=should_remove) elif should_remove: # Still add a remap, to remove the outputs. ctx.add_io_remap_removal(relay_name, inp_name) relay.remove()
def force_paintinmap(ctx: Context): """If paint entities are present, set paint in map to true.""" # Already set, don't bother confirming. if conv_bool(ctx.vmf.spawn['paintinmap']): return if needs_paint(ctx.vmf): ctx.vmf.spawn['paintinmap'] = '1' # Ensure we have some blobs. if conv_int(ctx.vmf.spawn['maxblobcount']) == 0: ctx.vmf.spawn['maxblobcount'] = '250'
def load(opt_blocks: Iterator[Property]): """Read settings from the given property block.""" SETTINGS.clear() set_vals = {} for opt_block in opt_blocks: for prop in opt_block: set_vals[prop.name] = prop.value options = {opt.id: opt for opt in DEFAULTS} if len(options) != len(DEFAULTS): from collections import Counter # Find ids used more than once.. raise Exception('Duplicate option(s)! ({})'.format(', '.join( k for k, v in Counter(opt.id for opt in DEFAULTS).items() if v > 1))) fallback_opts = [] for opt in DEFAULTS: try: val = set_vals.pop(opt.id) except KeyError: if opt.fallback is not None: fallback_opts.append(opt) assert opt.fallback in options, 'Invalid fallback in ' + opt.id else: SETTINGS[opt.id] = opt.default continue if opt.type is TYPE.VEC: # Pass nones so we can check if it failed.. parsed_vals = parse_vec_str(val, x=None) if parsed_vals[0] is None: SETTINGS[opt.id] = opt.default else: SETTINGS[opt.id] = Vec(*parsed_vals) elif opt.type is TYPE.BOOL: SETTINGS[opt.id] = srctools.conv_bool(val, opt.default) else: # int, float, str - no special handling... try: SETTINGS[opt.id] = opt.type.value(val) except (ValueError, TypeError): SETTINGS[opt.id] = opt.default for opt in fallback_opts: try: SETTINGS[opt.id] = SETTINGS[opt.fallback] except KeyError: raise Exception('Bad fallback for "{}"!'.format(opt.id)) # Check they have the same type. assert opt.type is options[opt.fallback].type if set_vals: LOGGER.warning('Extra config options: {}', set_vals)
def comp_trigger_coop(ctx: Context): """Creates a trigger which only activates with both players.""" for trig in ctx.vmf.by_class['comp_trigger_coop']: trig['classname'] = 'trigger_playerteam' trig['target_team'] = 0 only_once = conv_bool(trig['trigger_once']) trig['trigger_once'] = 0 trig_name = trig['targetname'] if not trig_name: # Give it something unique trig['targetname'] = trig_name = '_comp_trigger_coop_' + str( trig['hammer_id']) man_name = trig_name + '_man' manager = ctx.vmf.create_ent( classname='logic_coop_manager', origin=trig['origin'], targetname=man_name, # Should make it die if the trigger does. parentname=trig_name, ) for out in list(trig.outputs): folded_out = out.output.casefold() if folded_out == 'onstarttouchboth': out.output = 'OnChangeToAllTrue' elif folded_out == 'onendtouchboth': out.output = 'OnChangeToAnyFalse' else: continue trig.outputs.remove(out) manager.add_out(out) trig.add_out( Output('OnStartTouchBluePlayer', man_name, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', man_name, 'SetStateBrue'), Output('OnEndTouchBluePlayer', man_name, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', man_name, 'SetStateBFalse'), ) if only_once: manager.add_out( Output('OnChangeToAllTrue', man_name, 'Kill'), Output('OnChangeToAllTrue', trig_name, 'Kill'), ) # Only keep OnChangeToAllTrue outputs, and remove # them once they've fired. for out in list(manager): if out.output.casefold() == 'onchangetoalltrue': out.only_once = True else: manager.outputs.remove(out)
def comp_trigger_coop(ctx: Context): """Creates a trigger which only activates with both players.""" for trig in ctx.vmf.by_class['comp_trigger_coop']: trig['classname'] = 'trigger_playerteam' trig['target_team'] = 0 only_once = conv_bool(trig['trigger_once']) trig['trigger_once'] = 0 trig_name = trig['targetname'] if not trig_name: # Give it something unique trig['targetname'] = trig_name = '_comp_trigger_coop_' + str(trig['hammer_id']) man_name = trig_name + '_man' manager = ctx.vmf.create_ent( classname='logic_coop_manager', origin=trig['origin'], targetname=man_name, # Should make it die if the trigger does. parentname=trig_name, ) for out in list(trig.outputs): folded_out = out.output.casefold() if folded_out == 'onstarttouchboth': out.output = 'OnChangeToAllTrue' elif folded_out == 'onendtouchboth': out.output = 'OnChangeToAnyFalse' else: continue trig.outputs.remove(out) manager.add_out(out) trig.add_out( Output('OnStartTouchBluePlayer', man_name, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', man_name, 'SetStateBrue'), Output('OnEndTouchBluePlayer', man_name, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', man_name, 'SetStateBFalse'), ) if only_once: manager.add_out( Output('OnChangeToAllTrue', man_name, 'Kill'), Output('OnChangeToAllTrue', trig_name, 'Kill'), ) # Only keep OnChangeToAllTrue outputs, and remove # them once they've fired. for out in list(manager): if out.output.casefold() == 'onchangetoalltrue': out.only_once = True else: manager.outputs.remove(out)
def get_itemconf( name: Union[str, Tuple[str, str]], default: Optional[OptionType], timer_delay: int = None, ) -> Optional[OptionType]: """Get an itemconfig value. The name should be an 'ID:Section', or a tuple of the same. The type of the default sets what value it will be converted to. None returns the string, or None if not present. If set, timer_value is the value used for the timer. """ if name == '': return default try: if isinstance(name, tuple): group_id, wid_id = name else: group_id, wid_id = name.split(':') except ValueError: LOGGER.warning('Invalid item config: {!r}!', name) return default wid_id = wid_id.casefold() if timer_delay is not None: if timer_delay < 3 or timer_delay > 30: wid_id += '_inf' else: wid_id += '_{}'.format(timer_delay) value = ITEM_CONFIG.get_val(group_id, wid_id, '') if not value: return default if isinstance(default, str) or default is None: return value elif isinstance(default, Vec): return Vec.from_str(value, default.x, default.y, default.z) elif isinstance(default, bool): return srctools.conv_bool(value, default) elif isinstance(default, float): return srctools.conv_int(value, default) elif isinstance(default, int): return srctools.conv_int(value, default) else: raise TypeError('Invalid default type "{}"!'.format( type(default).__name__))
def res_change_inputs_setup(res: Property): vals = {} for prop in res: out_key = Output.parse_name(prop.real_name) if prop.has_children(): vals[out_key] = ( prop['inst_in', None], prop['input'], prop['params', ''], srctools.conv_float(prop['delay', 0.0]), 1 if srctools.conv_bool(prop['only_once', '0']) else -1, ) else: vals[out_key] = None return vals
def from_file(cls, path, zip_file): """Initialise from a file. path is the file path for the map inside the zip, without extension. zip_file is either a ZipFile or FakeZip object. """ # Some P2Cs may have non-ASCII characters in descriptions, so we # need to read it as bytes and convert to utf-8 ourselves - zips # don't convert encodings automatically for us. try: with zip_open_bin(zip_file, path + '.p2c') as file: props = Property.parse( # Decode the P2C as UTF-8, and skip unknown characters. # We're only using it for display purposes, so that should # be sufficent. TextIOWrapper( file, encoding='utf-8', errors='replace', ), path, ) except KeyValError: # Silently fail if we can't parse the file. That way it's still # possible to backup. LOGGER.warning('Failed parsing puzzle file!', path, exc_info=True) props = Property('portal2_puzzle', []) title = None desc = _('Failed to parse this puzzle file. It can still be backed up.') else: props = props.find_key('portal2_puzzle', []) title = props['title', None] desc = props['description', _('No description found.')] if title is None: title = '<' + path.rsplit('/', 1)[-1] + '.p2c>' return cls( filename=os.path.basename(path), zip_file=zip_file, title=title, desc=desc, is_coop=srctools.conv_bool(props['coop', '0']), create_time=Date(props['timestamp_created', '']), mod_time=Date(props['timestamp_modified', '']), )
def widget_checkmark(parent: tk.Frame, var: tk.StringVar, conf: Property): """Allows ticking a box.""" # Ensure it's a bool value. if conv_bool(var.get()): var.set('1') else: var.set('0') return ttk.Checkbutton( parent, text='', variable=var, onvalue='1', offvalue='0', command=widget_sfx, )
def flag_instvar(inst: Entity, flag: Property) -> bool: """Checks if the $replace value matches the given value. The flag value follows the form `A == B`, with any of the three permitted to be variables. The operator can be any of `=`, `==`, `<`, `>`, `<=`, `>=`, `!=`. If omitted, the operation is assumed to be `==`. If only a single value is present, it is tested as a boolean flag. """ values = flag.value.split(' ', 3) if len(values) == 3: val_a, op, val_b = values op = inst.fixup.substitute(op) comp_func = INSTVAR_COMP.get(op, operator.eq) elif len(values) == 2: val_a, val_b = values op = '==' comp_func = operator.eq else: # For just a name. return conv_bool(inst.fixup.substitute(values[0])) if '$' not in val_a and '$' not in val_b: # Handle pre-substitute behaviour, where val_a is always a var. LOGGER.warning( 'Comparison "{}" has no $var, assuming first value. ' 'Please use $ when referencing vars.', flag.value, ) val_a = '$' + val_a val_a = inst.fixup.substitute(val_a, default='') val_b = inst.fixup.substitute(val_b, default='') try: # Convert to floats if possible, otherwise handle both as strings. # That ensures we normalise different number formats (1 vs 1.0) val_a, val_b = float(val_a), float(val_b) except ValueError: pass try: return comp_func(val_a, val_b) except (TypeError, ValueError) as e: LOGGER.warning('InstVar comparison failed: {} {} {}', val_a, op, val_b, exc_info=e) return False
def res_cust_output_setup(res: Property): conds = [ Condition.parse(sub_res) for sub_res in res if sub_res.name == 'targcondition' ] outputs = list(res.find_all('addOut')) dec_con_count = srctools.conv_bool(res["decConCount", '0'], False) sign_type = IND_PANEL_TYPES.get(res['sign_type', None], None) if sign_type is None: sign_act = sign_deact = (None, '') else: # The outputs which trigger the sign. sign_act = Output.parse_name(res['sign_activate', '']) sign_deact = Output.parse_name(res['sign_deactivate', '']) return outputs, dec_con_count, conds, sign_type, sign_act, sign_deact
def res_hollow_brush(inst: Entity, res: Property): """Hollow out the attached brush, as if EmbeddedVoxel was set. This just removes the surface if it's already an embeddedVoxel. This allows multiple items to embed thinly in the same block without affecting each other. """ loc = Vec(0, 0, -64).rotate_by_str(inst['angles']) loc += Vec.from_str(inst['origin']) try: group = SOLIDS[loc.as_tuple()] except KeyError: LOGGER.warning('No brush for hollowing at ({})', loc) return # No brush here? conditions.hollow_block(group, remove_orig_face=srctools.conv_bool( res['RemoveFace', False]))
def main(): global OPTIMISE OPTIMISE = conv_bool(input('Optimise zips? ')) print('Optimising: ', OPTIMISE) zip_path = os.path.join( os.getcwd(), 'zips', 'sml' if OPTIMISE else 'lrg', ) if os.path.isdir(zip_path): for file in os.listdir(zip_path): print('Deleting', file) os.remove(os.path.join(zip_path, file)) else: os.makedirs(zip_path, exist_ok=True) shutil.rmtree('zips/hammer/', ignore_errors=True) path = os.path.join( os.getcwd(), 'packages\\', ) # A list of all the package zips. for package in search_folder(zip_path, path): build_package(*package) print('Building main zip...') pack_name = 'BEE{}_packages.zip'.format(input('Version: ')) with ZipFile(os.path.join('zips', pack_name), 'w', compression=ZIP_DEFLATED) as zip_file: for file in os.listdir(zip_path): zip_file.write(os.path.join(zip_path, file), os.path.join('packages/', file)) print('.', end='', flush=True) print('Done!')
def main(): global OPTIMISE OPTIMISE = conv_bool(input('Optimise zips? ')) print('Optimising: ', OPTIMISE) zip_path = os.path.join( os.getcwd(), 'zips', 'sml' if OPTIMISE else 'lrg', ) if os.path.isdir(zip_path): for file in os.listdir(zip_path): print('Deleting', file) os.remove(os.path.join(zip_path, file)) else: os.makedirs(zip_path, exist_ok=True) path = os.path.join( os.getcwd(), 'packages\\', ) # A list of all the package zips. packages = list(search_folder(zip_path, path)) with futures.ThreadPoolExecutor(10) as future: list(future.map(build_package, packages)) print('Building main zip...') pack_name = 'BEE{}_packages.zip'.format(input('Version: ')) with ZipFile(os.path.join('zips', pack_name), 'w', compression=ZIP_DEFLATED) as zip_file: for file in os.listdir(zip_path): zip_file.write(os.path.join(zip_path, file), os.path.join('packages/', file)) print('.', end='', flush=True) print('Done!')
def flag_angles(inst: Entity, flag: Property): """Check that a instance is pointed in a direction. The value should be either just the angle to check, or a block of options: - `Angle`: A unit vector (XYZ value) pointing in a direction, or some keywords: `+z`, `-y`, `N`/`S`/`E`/`W`, `up`/`down`, `floor`/`ceiling`, or `walls` for any wall side. - `From_dir`: The direction the unrotated instance is pointed in. This lets the flag check multiple directions - `Allow_inverse`: If true, this also returns True if the instance is pointed the opposite direction . """ angle = inst['angles', '0 0 0'] if flag.has_children(): targ_angle = flag['direction', '0 0 0'] from_dir = flag['from_dir', '0 0 1'] if from_dir.casefold() in DIRECTIONS: from_dir = Vec(DIRECTIONS[from_dir.casefold()]) else: from_dir = Vec.from_str(from_dir, 0, 0, 1) allow_inverse = srctools.conv_bool(flag['allow_inverse', '0']) else: targ_angle = flag.value from_dir = Vec(0, 0, 1) allow_inverse = False normal = DIRECTIONS.get(targ_angle.casefold(), None) if normal is None: return False # If it's not a special angle, # so it failed the exact match inst_normal = from_dir.rotate_by_str(angle) if normal == 'WALL': # Special case - it's not on the floor or ceiling return not (inst_normal == (0, 0, 1) or inst_normal == (0, 0, -1)) else: return inst_normal == normal or ( allow_inverse and -inst_normal == normal )
def res_rand_num(inst: Entity, res: Property) -> None: """Generate a random number and save in a fixup value. If 'decimal' is true, the value will contain decimals. 'max' and 'min' are inclusive. 'ResultVar' is the variable the result will be saved in. If 'seed' is set, it will be used to keep the value constant across map recompiles. This should be unique. """ is_float = srctools.conv_bool(res['decimal']) max_val = srctools.conv_float(res['max', 1.0]) min_val = srctools.conv_float(res['min', 0.0]) var = res['resultvar', '$random'] seed = 'd' + res['seed', 'random'] set_random_seed(inst, seed) if is_float: func = random.uniform else: func = random.randint inst.fixup[var] = str(func(min_val, max_val))
def precache_light_bridge(ctx: Context): """Ensure light bridges have the particle precached.""" for bridge in ctx.vmf.by_class['prop_wall_projector']: if conv_bool(bridge['StartEnabled', '0']): return # Starts on, no need. break else: # No bridges in the map. return for part in ctx.vmf.by_class['info_particle_system']: # Check for users already fixing the problem. if part['effect_name'].casefold() == 'projected_wall_impact': return ctx.vmf.create_ent( classname='info_particle_system', origin='-15872 -15872 -15872', effect_name='projected_wall_impact', start_active='0', )
def main(): global OPTIMISE gen_vpks() OPTIMISE = conv_bool(input('Optimise zips? ')) print('Optimising: ', OPTIMISE) zip_path = os.path.join( os.getcwd(), 'zips', 'sml' if OPTIMISE else 'lrg', ) if os.path.isdir(zip_path): for file in os.listdir(zip_path): print('Deleting', file) os.remove(os.path.join(zip_path, file)) else: os.makedirs(zip_path, exist_ok=True) path = os.path.join(os.getcwd(), 'packages\\', ) # A list of all the package zips. packages = list(search_folder(zip_path, path)) with futures.ThreadPoolExecutor(10) as future: list(future.map(build_package, packages)) print('Building main zip...') pack_name = 'BEE{}_packages.zip'.format(input('Version: ')) with ZipFile(os.path.join('zips', pack_name), 'w', compression=ZIP_DEFLATED) as zip_file: for file in os.listdir(zip_path): zip_file.write(os.path.join(zip_path, file), os.path.join('packages/', file)) print('.', end='', flush=True) print('Done!')
def res_add_global_inst(res: Property): """Add one instance in a location. Options: allow_multiple: Allow multiple copies of this instance. If 0, the instance will not be added if it was already added. name: The targetname of the instance. IF blank, the instance will be given a name of the form 'inst_1234'. file: The filename for the instance. Angles: The orientation of the instance (defaults to '0 0 0'). Origin: The location of the instance (defaults to '0 0 -10000'). Fixup_style: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. """ if res.value is not None: if srctools.conv_bool(res["allow_multiple", "0"]) or res["file"] not in GLOBAL_INSTANCES: # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = Entity( vbsp.VMF, keys={ "classname": "func_instance", "targetname": res["name", ""], "file": resolve_inst(res["file"])[0], "angles": res["angles", "0 0 0"], "origin": res["position", "0 0 -10000"], "fixup_style": res["fixup_style", "0"], }, ) GLOBAL_INSTANCES.add(res["file"]) if new_inst["targetname"] == "": new_inst["targetname"] = "inst_" new_inst.make_unique() vbsp.VMF.add_ent(new_inst) return RES_EXHAUSTED
def main(): global OPTIMISE OPTIMISE = conv_bool(input('Optimise zips? ')) print('Optimising: ', OPTIMISE) zip_path = os.path.join( os.getcwd(), 'zips', 'sml' if OPTIMISE else 'lrg', ) if os.path.isdir(zip_path): for file in os.listdir(zip_path): print('Deleting', file) os.remove(os.path.join(zip_path, file)) else: os.makedirs(zip_path, exist_ok=True) shutil.rmtree('zips/hammer/', ignore_errors=True) path = os.path.join(os.getcwd(), 'packages\\', ) # A list of all the package zips. for package in search_folder(zip_path, path): build_package(*package) print('Building main zip...') pack_name = 'BEE{}_packages.zip'.format(input('Version: ')) with ZipFile(os.path.join('zips', pack_name), 'w', compression=ZIP_DEFLATED) as zip_file: for file in os.listdir(zip_path): zip_file.write(os.path.join(zip_path, file), os.path.join('packages/', file)) print('.', end='', flush=True) print('Done!')
def needs_paint(vmf: VMF) -> bool: """Check if we have paint.""" for ent_cls in [ 'prop_paint_bomb', 'paint_sphere', 'weapon_paintgun', # Not in retail but someone might add it. ]: if vmf.by_class[ent_cls]: return True for ent in vmf.by_class['info_paint_sprayer']: # Special case, this makes sprayers only render visually, which # works even without the value set. if not conv_bool(ent['DrawOnly']): return True for ent_cls in [ 'prop_weighted_cube', 'prop_physics_paintable', ]: for ent in vmf.by_class[ent_cls]: # If the cube is bouncy, enable paint. if conv_int(ent['paintpower', '4'], 4) != 4: return True
def res_translate_inst(inst: Entity, res: Property): """Translate the instance locally by the given amount. The special values <piston>, <piston_bottom> and <piston_top> can be used to offset it based on the starting position, bottom or top position of a piston platform. """ folded_val = res.value.casefold() if folded_val == '<piston>': folded_val = ( '<piston_top>' if srctools.conv_bool(inst.fixup['$start_up']) else '<piston_bottom>' ) if folded_val == '<piston_top>': val = Vec(z=128 * srctools.conv_int(inst.fixup['$top_level', '1'], 1)) elif folded_val == '<piston_bottom>': val = Vec(z=128 * srctools.conv_int(inst.fixup['$bottom_level', '0'], 0)) else: val = Vec.from_str(res.value) offset = val.rotate_by_str(inst['angles']) inst['origin'] = (offset + Vec.from_str(inst['origin'])).join(' ')
def res_cutout_tile(res: Property): """Generate random quarter tiles, like in Destroyed or Retro maps. - "MarkerItem" is the instance to look for. - "TileSize" can be "2x2" or "4x4". - rotateMax is the amount of degrees to rotate squarebeam models. Materials: - "squarebeams" is the squarebeams variant to use. - "ceilingwalls" are the sides of the ceiling section. - "floorbase" is the texture under floor sections. - "tile_glue" is used on top of a thinner tile segment. - "clip" is the player_clip texture used over floor segments. (This allows customising the surfaceprop.) - "Floor4x4Black", "Ceil2x2White" and other combinations can be used to override the textures used. """ item = resolve_inst(res['markeritem']) INST_LOCS = {} # Map targetnames -> surface loc CEIL_IO = [] # Pairs of ceil inst corners to cut out. FLOOR_IO = [] # Pairs of floor inst corners to cut out. overlay_ids = {} # When we replace brushes, we need to fix any overlays # on that surface. MATS.clear() floor_edges = [] # Values to pass to add_floor_sides() at the end sign_loc = set(FORCE_LOCATIONS) # If any signage is present in the map, we need to force tiles to # appear at that location! for over in conditions.VMF.by_class['info_overlay']: if ( over['material'].casefold() in FORCE_TILE_MATS and # Only check floor/ceiling overlays over['basisnormal'] in ('0 0 1', '0 0 -1') ): loc = Vec.from_str(over['origin']) # Sometimes (light bridges etc) a sign will be halfway between # tiles, so in that case we need to force 2 tiles. loc_min = (loc - (15, 15, 0)) // 32 * 32 # type: Vec loc_max = (loc + (15, 15, 0)) // 32 * 32 # type: Vec loc_min += (16, 16, 0) loc_max += (16, 16, 0) FORCE_LOCATIONS.add(loc_min.as_tuple()) FORCE_LOCATIONS.add(loc_max.as_tuple()) SETTINGS = { 'floor_chance': srctools.conv_int( res['floorChance', '100'], 100), 'ceil_chance': srctools.conv_int( res['ceilingChance', '100'], 100), 'floor_glue_chance': srctools.conv_int( res['floorGlueChance', '0']), 'ceil_glue_chance': srctools.conv_int( res['ceilingGlueChance', '0']), 'rotate_beams': int(srctools.conv_float( res['rotateMax', '0']) * BEAM_ROT_PRECISION), 'beam_skin': res['squarebeamsSkin', '0'], 'base_is_disp': srctools.conv_bool(res['dispBase', '0']), 'quad_floor': res['FloorSize', '4x4'].casefold() == '2x2', 'quad_ceil': res['CeilingSize', '4x4'].casefold() == '2x2', } random.seed(vbsp.MAP_RAND_SEED + '_CUTOUT_TILE_NOISE') noise = SimplexNoise(period=4 * 40) # 4 tiles/block, 50 blocks max # We want to know the number of neighbouring tile cutouts before # placing tiles - blocks away from the sides generate fewer tiles. floor_neighbours = defaultdict(dict) # all_floors[z][x,y] = count for mat_prop in res['Materials', []]: MATS[mat_prop.name].append(mat_prop.value) if SETTINGS['base_is_disp']: # We want the normal brushes to become nodraw. MATS['floorbase_disp'] = MATS['floorbase'] MATS['floorbase'] = ['tools/toolsnodraw'] # Since this uses random data for initialisation, the alpha and # regular will use slightly different patterns. alpha_noise = SimplexNoise(period=4 * 50) else: alpha_noise = None for key, default in TEX_DEFAULT: if key not in MATS: MATS[key] = [default] # Find our marker ents for inst in conditions.VMF.by_class['func_instance']: # type: VLib.Entity if inst['file'].casefold() not in item: continue targ = inst['targetname'] orient = Vec(0, 0, 1).rotate_by_str(inst['angles', '0 0 0']) # Check the orientation of the marker to figure out what to generate if orient == (0, 0, 1): io_list = FLOOR_IO else: io_list = CEIL_IO # Reuse orient to calculate where the solid face will be. loc = (orient * -64) + Vec.from_str(inst['origin']) INST_LOCS[targ] = loc for out in inst.output_targets(): io_list.append((targ, out)) if not inst.outputs and inst.fixup['$connectioncount'] == '0': # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 128x128 tile segment. io_list.append((targ, targ)) inst.remove() # Remove the instance itself from the map. for start_floor, end_floor in FLOOR_IO: if end_floor not in INST_LOCS: # Not a marker - remove this and the antline. for toggle in conditions.VMF.by_target[end_floor]: conditions.remove_ant_toggle(toggle) continue box_min = Vec(INST_LOCS[start_floor]) box_min.min(INST_LOCS[end_floor]) box_max = Vec(INST_LOCS[start_floor]) box_max.max(INST_LOCS[end_floor]) if box_min.z != box_max.z: continue # They're not in the same level! z = box_min.z if SETTINGS['rotate_beams']: # We have to generate 1 model per 64x64 block to do rotation... gen_rotated_squarebeams( box_min - (64, 64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin'], max_rot=SETTINGS['rotate_beams'], ) else: # Make the squarebeams props, using big models if possible gen_squarebeams( box_min + (-64, -64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin'] ) # Add a player_clip brush across the whole area conditions.VMF.add_brush(conditions.VMF.make_prism( p1=box_min - (64, 64, FLOOR_DEPTH), p2=box_max + (64, 64, 0), mat=MATS['clip'][0], ).solid) # Add a noportal_volume covering the surface, in case there's # room for a portal. noportal_solid = conditions.VMF.make_prism( # Don't go all the way to the sides, so it doesn't affect wall # brushes. p1=box_min - (63, 63, 9), p2=box_max + (63, 63, 0), mat='tools/toolsinvisible', ).solid noportal_ent = conditions.VMF.create_ent( classname='func_noportal_volume', origin=box_min.join(' '), ) noportal_ent.solids.append(noportal_solid) if SETTINGS['base_is_disp']: # Use displacements for the base instead. make_alpha_base( box_min + (-64, -64, 0), box_max + (64, 64, 0), noise=alpha_noise, ) for x, y in utils.iter_grid( min_x=int(box_min.x), max_x=int(box_max.x) + 1, min_y=int(box_min.y), max_y=int(box_max.y) + 1, stride=128, ): # Build the set of all positions.. floor_neighbours[z][x, y] = -1 # Mark borders we need to fill in, and the angle (for func_instance) # The wall is the face pointing inwards towards the bottom brush, # and the ceil is the ceiling of the block above the bordering grid # points. for x in range(int(box_min.x), int(box_max.x) + 1, 128): # North floor_edges.append(BorderPoints( wall=Vec(x, box_max.y + 64, z - 64), ceil=Vec_tuple(x, box_max.y + 128, z), rot=270, )) # South floor_edges.append(BorderPoints( wall=Vec(x, box_min.y - 64, z - 64), ceil=Vec_tuple(x, box_min.y - 128, z), rot=90, )) for y in range(int(box_min.y), int(box_max.y) + 1, 128): # East floor_edges.append(BorderPoints( wall=Vec(box_max.x + 64, y, z - 64), ceil=Vec_tuple(box_max.x + 128, y, z), rot=180, )) # West floor_edges.append(BorderPoints( wall=Vec(box_min.x - 64, y, z - 64), ceil=Vec_tuple(box_min.x - 128, y, z), rot=0, )) # Now count boundries near tiles, then generate them. # Do it seperately for each z-level: for z, xy_dict in floor_neighbours.items(): # type: float, dict for x, y in xy_dict: # type: float, float # We want to count where there aren't any tiles xy_dict[x, y] = ( ((x - 128, y - 128) not in xy_dict) + ((x - 128, y + 128) not in xy_dict) + ((x + 128, y - 128) not in xy_dict) + ((x + 128, y + 128) not in xy_dict) + ((x - 128, y) not in xy_dict) + ((x + 128, y) not in xy_dict) + ((x, y - 128) not in xy_dict) + ((x, y + 128) not in xy_dict) ) max_x = max_y = 0 weights = {} # Now the counts are all correct, compute the weight to apply # for tiles. # Adding the neighbouring counts will make a 5x5 area needed to set # the center to 0. for (x, y), cur_count in xy_dict.items(): max_x = max(x, max_x) max_y = max(y, max_y) # Orthrogonal is worth 0.2, diagonal is worth 0.1. # Not-present tiles would be 8 - the maximum tile_count = ( 0.8 * cur_count + 0.1 * xy_dict.get((x - 128, y - 128), 8) + 0.1 * xy_dict.get((x - 128, y + 128), 8) + 0.1 * xy_dict.get((x + 128, y - 128), 8) + 0.1 * xy_dict.get((x + 128, y + 128), 8) + 0.2 * xy_dict.get((x - 128, y), 8) + 0.2 * xy_dict.get((x, y - 128), 8) + 0.2 * xy_dict.get((x, y + 128), 8) + 0.2 * xy_dict.get((x + 128, y), 8) ) # The number ranges from 0 (all tiles) to 12.8 (no tiles). # All tiles should still have a small chance to generate tiles. weights[x, y] = min((tile_count + 0.5) / 8, 1) # Share the detail entity among same-height tiles.. detail_ent = conditions.VMF.create_ent( classname='func_detail', ) for x, y in xy_dict: convert_floor( Vec(x, y, z), overlay_ids, MATS, SETTINGS, sign_loc, detail_ent, noise_weight=weights[x, y], noise_func=noise, ) add_floor_sides(floor_edges) conditions.reallocate_overlays(overlay_ids) return conditions.RES_EXHAUSTED
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_fizzler_pair(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. """ 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_file = res['StartInst', orig_file] end_file = res['EndInst', orig_file] mid_file = res['MidInst', ''] single_file = res['SingleInst', ''] begin_inst['file'] = begin_file begin_inst['targetname'] = pair_name 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 single_file and length == 0: end_inst.remove() begin_inst['file'] = single_file return end_inst['targetname'] = pair_name end_inst['file'] = end_file if mid_file != '': # 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.join(' '), )
def res_unst_scaffold(res: Property): """The condition to generate Unstationary Scaffolds. This is executed once to modify all instances. """ # The instance types we're modifying if res.value not in SCAFFOLD_CONFIGS: # We've already executed this config group return RES_EXHAUSTED LOGGER.info("Running Scaffold Generator ({})...", res.value) TARG_INST, LINKS = SCAFFOLD_CONFIGS[res.value] del SCAFFOLD_CONFIGS[res.value] # Don't let this run twice instances = {} # Find all the instances we're wanting to change, and map them to # targetnames for ent in vbsp.VMF.by_class["func_instance"]: file = ent["file"].casefold() targ = ent["targetname"] if file not in TARG_INST: continue config = TARG_INST[file] next_inst = set(out.target for out in ent.outputs) # Destroy these outputs, they're useless now! ent.outputs.clear() instances[targ] = {"ent": ent, "conf": config, "next": next_inst, "prev": None} # Now link each instance to its in and outputs for targ, inst in instances.items(): scaff_targs = 0 for ent_targ in inst["next"]: if ent_targ in instances: instances[ent_targ]["prev"] = targ inst["next"] = ent_targ scaff_targs += 1 else: # If it's not a scaffold, it's probably an indicator_toggle. # We want to remove any them as well as the assoicated # antlines! for toggle in vbsp.VMF.by_target[ent_targ]: conditions.remove_ant_toggle(toggle) if scaff_targs > 1: raise Exception("A scaffold item has multiple destinations!") elif scaff_targs == 0: inst["next"] = None # End instance starting_inst = [] # We need to find the start instances, so we can set everything up for inst in instances.values(): if inst["prev"] is None and inst["next"] is None: # Static item! continue elif inst["prev"] is None: starting_inst.append(inst) # We need to make the link entities unique for each scaffold set, # otherwise the AllVar property won't work. group_counter = 0 # Set all the instances and properties for start_inst in starting_inst: group_counter += 1 ent = start_inst["ent"] for vals in LINKS.values(): if vals["all"] is not None: ent.fixup[vals["all"]] = SCAFF_PATTERN.format(name=vals["name"], group=group_counter, index="*") should_reverse = srctools.conv_bool(ent.fixup["$start_reversed"]) # Now set each instance in the chain, including first and last for index, inst in enumerate(scaff_scan(instances, start_inst)): ent, conf = inst["ent"], inst["conf"] orient = "floor" if Vec(0, 0, 1).rotate_by_str(ent["angles"]) == (0, 0, 1) else "wall" # Find the offset used for the logic ents offset = (conf["off_" + orient]).copy() if conf["is_piston"]: # Adjust based on the piston position offset.z += 128 * srctools.conv_int( ent.fixup["$top_level" if ent.fixup["$start_up"] == "1" else "$bottom_level"] ) offset.rotate_by_str(ent["angles"]) offset += Vec.from_str(ent["origin"]) if inst["prev"] is None: link_type = "start" elif inst["next"] is None: link_type = "end" else: link_type = "mid" if orient == "floor" and link_type != "mid" and conf["inst_end"] is not None: # Add an extra instance pointing in the direction # of the connected track. This would be the endcap # model. other_ent = instances[inst["next" if link_type == "start" else "prev"]]["ent"] other_pos = Vec.from_str(other_ent["origin"]) our_pos = Vec.from_str(ent["origin"]) link_dir = other_pos - our_pos link_ang = math.degrees(math.atan2(link_dir.y, link_dir.x)) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals link_ang = (link_ang + 45) // 90 * 90 vbsp.VMF.create_ent( classname="func_instance", targetname=ent["targetname"], file=conf["inst_end"], origin=offset.join(" "), angles="0 {:.0f} 0".format(link_ang), ) # Don't place the offset instance, this replaces that! elif conf["inst_offset"] is not None: # Add an additional rotated entity at the offset. # This is useful for the piston item. vbsp.VMF.create_ent( classname="func_instance", targetname=ent["targetname"], file=conf["inst_offset"], origin=offset.join(" "), angles=ent["angles"], ) logic_inst = vbsp.VMF.create_ent( classname="func_instance", targetname=ent["targetname"], file=conf.get("logic_" + link_type + ("_rev" if should_reverse else ""), ""), origin=offset.join(" "), angles=("0 0 0" if conf["rotate_logic"] else ent["angles"]), ) for key, val in ent.fixup.items(): # Copy over fixup values logic_inst.fixup[key] = val # Add the link-values for linkVar, link in LINKS.items(): logic_inst.fixup[linkVar] = SCAFF_PATTERN.format(name=link["name"], group=group_counter, index=index) if inst["next"] is not None: logic_inst.fixup[link["next"]] = SCAFF_PATTERN.format( name=link["name"], group=group_counter, index=index + 1 ) new_file = conf.get("inst_" + orient, "") if new_file != "": ent["file"] = new_file LOGGER.info("Finished Scaffold generation!") return RES_EXHAUSTED
def clean_vmf(vmf_path): """Optimise the VMFs, removing unneeded entities or objects.""" inst = VMF.parse(vmf_path) for ent in itertools.chain([inst.spawn], inst.entities[:]): editor = ent.editor # Remove useless metadata for cat in ('comments', 'color', 'logicalpos'): if cat in editor: del editor[cat] # Remove entities that have their visgroups hidden. if ent.hidden or not conv_bool(editor.get('visgroupshown', '1'), True): print('Removing hidden ent') inst.remove_ent(ent) continue # Remove info_null entities if ent['classname'] == 'info_null': print('Removing info_null...') inst.remove_ent(ent) continue # All instances must be in bee2/, so any reference outside there is a map error! # It's ok if it's in p2editor and not in a subfolder though. # There's also an exception needed for the Tag gun instance. if ent['classname'] == 'func_instance': inst_loc = ent['file'].casefold().replace('\\','/') if not inst_loc.startswith('instances/bee2/') and not (inst_loc.startswith('instances/p2editor/') and inst_loc.count('/') == 2) and 'alatag' not in inst_loc: input('Invalid instance path "{}" in\n"{}"! Press Enter to continue..'.format(ent['file'], vmf_path)) yield from clean_vmf(vmf_path) # Re-run so we check again.. for solid in ent.solids[:]: if all(face.mat.casefold() == 'tools/toolsskip' for face in solid): print('Removing SKIP brush') ent.solids.remove(solid) continue if solid.hidden or not conv_bool(solid.editor.get('visgroupshown', '1'), True): print('Removing hidden brush') ent.solids.remove(solid) continue for detail in inst.by_class['func_detail']: # Remove several unused default options from func_detail. # We're not on xbox! del detail['disableX360'] # These aren't used in any instances, and it doesn't seem as if # VBSP preserves these values anyway. del detail['maxcpulevel'], detail['mincpulevel'] del detail['maxgpulevel'], detail['mingpulevel'] # Since all VMFs are instances or similar (not complete maps), we'll never # use worldspawn's settings. Keep mapversion though. del inst.spawn['maxblobcount'], inst.spawn['maxpropscreenwidth'] del inst.spawn['maxblobcount'], del inst.spawn['detailvbsp'], inst.spawn['detailmaterial'] lines = inst.export(inc_version=False, minimal=True).splitlines() for line in lines: yield line.lstrip()
def flag_brush_at_loc(vmf: VMF, inst: Entity, flag: Property): """Checks to see if a wall is present at the given location. - `Pos` is the position of the brush, where `0 0 0` is the floor-position of the brush. - `Dir` is the normal the face is pointing. `(0 0 -1)` is up. - `Type` defines the type the brush must be: - `Any` requires either a black or white brush. - `None` means that no brush must be present. - `White` requires a portalable surface. - `Black` requires a non-portalable surface. - `SetVar` defines an instvar which will be given a value of `black`, `white` or `none` to allow the result to be reused. - If `gridPos` is true, the position will be snapped so it aligns with the 128 grid (Useful with fizzler/light strip items). - `RemoveBrush`: If set to `1`, the brush will be removed if found. Only do this to `EmbedFace` brushes, since it will remove the other sides as well. """ pos = Vec.from_str(flag['pos', '0 0 0']) pos.z -= 64 # Subtract so origin is the floor-position pos = pos.rotate_by_str(inst['angles', '0 0 0']) # Relative to the instance origin pos += Vec.from_str(inst['origin', '0 0 0']) norm = flag['dir', None] if norm is not None: norm = Vec.from_str(norm).rotate_by_str( inst['angles', '0 0 0'], ) if srctools.conv_bool(flag['gridpos', '0']) and norm is not None: for axis in 'xyz': # Don't realign things in the normal's axis - # those are already fine. if norm[axis] == 0: pos[axis] = pos[axis] // 128 * 128 + 64 result_var = flag['setVar', ''] should_remove = srctools.conv_bool(flag['RemoveBrush', False], False) des_type = flag['type', 'any'].casefold() brush = SOLIDS.get(pos.as_tuple(), None) if brush is None or (norm is not None and abs(brush.normal) != abs(norm)): br_type = 'none' else: br_type = str(brush.color) if should_remove: vmf.remove_brush( brush.solid, ) if result_var: inst.fixup[result_var] = br_type if des_type == 'any' and br_type != 'none': return True return des_type == br_type
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 sceneset(ctx: Context): """Chains a set of choreographed scenes together.""" for ent in ctx.vmf.by_class['comp_choreo_sceneset']: scenes = [ ent['scene{:02}'.format(i)] for i in range(1, 21) if ent['scene{:02}'.format(i)] ] if not scenes: LOGGER.warning( '"{}" at ({}) has no scenes!', ent['targetname'], ent['origin'], ) continue if conv_bool(ent['play_dings']): scenes.insert(0, 'scenes/npc/glados_manual/ding_on.vcd') scenes.append('scenes/npc/glados_manual/ding_off.vcd') delay = conv_float(ent['delay'], 0.1) only_once = conv_bool(ent['only_once']) ent.remove() start_ent = None name = ent['targetname'] or '_choreo_{}'.format(ent.id) for i, scene in enumerate(scenes): part = ctx.vmf.create_ent( classname='logic_choreographed_scene', targetname=( '{}_{}'.format(name, i) if i > 0 else name ), origin=ent['origin'], scenefile=scene, ) if i + 1 < len(scenes): part.add_out(Output( 'OnCompletion', '{}_{}'.format(name, i+1), 'Start', delay=delay, )) if only_once: # When started blank the name so it can't be triggered, # then clean up after finished part.add_out( Output('OnStart', '!self', 'AddOutput', 'targetname '), Output('OnCompletion', '!self', 'Kill'), ) if start_ent is None: start_ent = part assert start_ent is not None, "Has scenes but none made?" for out in ent.outputs: if out.output.casefold() == 'onstart': start_ent.add_out(out) elif out.output.casefold() == 'onfinish': # Part is the last in the loop. out.output = 'OnCompletion' part.add_out(out)
def mod_screenshots() -> None: """Modify the map's screenshot.""" mod_type = CONF['screenshot_type', 'PETI'].lower() if mod_type == 'cust': LOGGER.info('Using custom screenshot!') scr_loc = CONF['screenshot', ''] elif mod_type == 'auto': LOGGER.info('Using automatic screenshot!') scr_loc = None # The automatic screenshots are found at this location: auto_path = os.path.join( '..', GAME_FOLDER.get(CONF['game_id', ''], 'portal2'), 'screenshots' ) # We need to find the most recent one. If it's named # "previewcomplete", we want to ignore it - it's a flag # to indicate the map was playtested correctly. try: screens = [ os.path.join(auto_path, path) for path in os.listdir(auto_path) ] except FileNotFoundError: # The screenshot folder doesn't exist! screens = [] screens.sort( key=os.path.getmtime, reverse=True, # Go from most recent to least ) playtested = False for scr_shot in screens: filename = os.path.basename(scr_shot) if filename.startswith('bee2_playtest_flag'): # Previewcomplete is a flag to indicate the map's # been playtested. It must be newer than the screenshot playtested = True continue elif filename.startswith('bee2_screenshot'): continue # Ignore other screenshots # We have a screenshot. Check to see if it's # not too old. (Old is > 2 hours) date = datetime.fromtimestamp( os.path.getmtime(scr_shot) ) diff = datetime.now() - date if diff.total_seconds() > 2 * 3600: LOGGER.info( 'Screenshot "{scr}" too old ({diff!s})', scr=scr_shot, diff=diff, ) continue # If we got here, it's a good screenshot! LOGGER.info('Chosen "{}"', scr_shot) LOGGER.info('Map Playtested: {}', playtested) scr_loc = scr_shot break else: # If we get to the end, we failed to find an automatic # screenshot! LOGGER.info('No Auto Screenshot found!') mod_type = 'peti' # Suppress the "None not found" error if srctools.conv_bool(CONF['clean_screenshots', '0']): LOGGER.info('Cleaning up screenshots...') # Clean up this folder - otherwise users will get thousands of # pics in there! for screen in screens: if screen != scr_loc and os.path.isfile(screen): os.remove(screen) LOGGER.info('Done!') else: # PeTI type, or something else scr_loc = None if scr_loc is not None and os.path.isfile(scr_loc): # We should use a screenshot! for screen in find_screenshots(): LOGGER.info('Replacing "{}"...', screen) # Allow us to edit the file... utils.unset_readonly(screen) shutil.copy(scr_loc, screen) # Make the screenshot readonly, so P2 can't replace it. # Then it'll use our own utils.set_readonly(screen) else: if mod_type != 'peti': # Error if we were looking for a screenshot LOGGER.warning('"{}" not found!', scr_loc) LOGGER.info('Using PeTI screenshot!') for screen in find_screenshots(): # Make the screenshot writeable, so P2 will replace it LOGGER.info('Making "{}" replaceable...', screen) utils.unset_readonly(screen)
def show_window(used_props, parent, item_name): global is_open, last_angle propList[:] = [key.casefold() for key in used_props] is_open = True spec_row = 1 start_up = srctools.conv_bool(used_props.get('startup', '0')) values['startup'] = start_up for prop, value in used_props.items(): if prop not in PROP_TYPES: LOGGER.info('Unknown property type {}', prop) continue if value is None: continue prop_type = PROP_TYPES[prop][0] if prop_type is PropTypes.CHECKBOX: values[prop].set(srctools.conv_bool(value)) elif prop_type is PropTypes.OSCILLATE: values[prop].set(srctools.conv_bool(value)) save_rail(prop) elif prop_type is PropTypes.GELS: values[prop].set(value) elif prop_type is PropTypes.PANEL: last_angle = value[5:7] values[prop].set(last_angle) out_values[prop] = value elif prop_type is PropTypes.PISTON: values[prop] = value try: top_level = int(used_props.get('toplevel', 4)) bot_level = int(used_props.get('bottomlevel', 0)) except ValueError: pass else: if ((prop == 'toplevel' and start_up) or (prop == 'bottomlevel' and not start_up)): widgets[prop].set( max( top_level, bot_level, ) ) if ((prop == 'toplevel' and not start_up) or (prop == 'bottomlevel' and start_up)): widgets[prop].set( min( top_level, bot_level, ) ) elif prop_type is PropTypes.TIMER: try: values[prop] = int(value) widgets[prop].set(values[prop]) except ValueError: pass elif not prop_type.is_editable: # Internal or subtype properties, just pass through unchanged. values[prop] = value else: LOGGER.error('Bad prop_type ({}) for {}', prop_type, prop) for key in PROP_POS_SPECIAL: if key in propList: labels[key].grid( row=spec_row, column=0, sticky=E, padx=2, pady=5, ) widgets[key].grid( row=spec_row, column=1, sticky="EW", padx=2, pady=5, columnspan=9, ) spec_row += 1 else: labels[key].grid_remove() widgets[key].grid_remove() # if we have a 'special' prop, add the divider between the types if spec_row > 1: widgets['div_h'].grid( row=spec_row + 1, columnspan=9, sticky="EW", ) spec_row += 2 else: widgets['div_h'].grid_remove() ind = 0 for key in PROP_POS: # Position each widget if key in propList: labels[key].grid( row=(ind // 3) + spec_row, column=(ind % 3) * 3, sticky=E, padx=2, pady=5, ) widgets[key].grid( row=(ind // 3) + spec_row, column=(ind % 3)*3 + 1, sticky="EW", padx=2, pady=5, ) ind += 1 else: labels[key].grid_remove() widgets[key].grid_remove() if ind > 1: # is there more than 1 checkbox? (add left divider) widgets['div_1'].grid( row=spec_row, column=2, sticky="NS", rowspan=(ind//3) + 1 ) else: widgets['div_1'].grid_remove() if ind > 2: # are there more than 2 checkboxes? (add right divider) widgets['div_2'].grid( row=spec_row, column=5, sticky="NS", rowspan=(ind//3) + 1, ) else: widgets['div_2'].grid_remove() if ind + spec_row == 1: # There aren't any items, display error message labels['noOptions'].grid(row=1, columnspan=9) ind = 1 else: labels['noOptions'].grid_remove() widgets['saveButton'].grid( row=ind + spec_row, columnspan=9, sticky="EW", ) # Block sound for the first few millisec to stop excess sounds from # playing sound.block_fx() widgets['titleLabel'].configure(text='Settings for "' + item_name + '"') win.title('BEE2 - ' + item_name) win.deiconify() win.lift(parent) win.grab_set() win.attributes("-topmost", True) win.geometry( '+' + str(parent.winfo_rootx() - 30) + '+' + str(parent.winfo_rooty() - win.winfo_reqheight() - 30) ) if contextWin.is_open: # Temporarily hide the context window while we're open. contextWin.prop_window.withdraw()