def parse(cls, data): """Parse an item definition.""" versions = {} def_version = None folders = {} unstyled = utils.conv_bool(data.info['unstyled', '0']) glob_desc = list(desc_parse(data.info)) desc_last = utils.conv_bool(data.info['AllDescLast', '0']) all_config = get_config( data.info, data.zip_file, 'items', pak_id=data.pak_id, prop_name='all_conf', ) needs_unlock = utils.conv_bool(data.info['needsUnlock', '0']) for ver in data.info.find_all('version'): vals = { 'name': ver['name', 'Regular'], 'id': ver['ID', 'VER_DEFAULT'], 'is_wip': utils.conv_bool(ver['wip', '0']), 'is_dep': utils.conv_bool(ver['deprecated', '0']), 'styles': {}, 'def_style': None, } for sty_list in ver.find_all('styles'): for sty in sty_list: if vals['def_style'] is None: vals['def_style'] = sty.value vals['styles'][sty.real_name] = sty.value folders[sty.value] = True versions[vals['id']] = vals if def_version is None: def_version = vals parse_item_folder(folders, data.zip_file, data.pak_id) for ver in versions.values(): if ver['def_style'] in folders: ver['def_style'] = folders[ver['def_style']] for sty, fold in ver['styles'].items(): ver['styles'][sty] = folders[fold] if not versions: raise ValueError('Item "' + data.id + '" has no versions!') return cls( data.id, versions=versions, def_version=def_version, needs_unlock=needs_unlock, all_conf=all_config, unstyled=unstyled, glob_desc=glob_desc, desc_last=desc_last, )
def flag_brush_at_loc(inst, flag): """Checks to see if a wall is present at the given location. - Pos is the position of the brush, where `0 0 0` is the floor-position of the brush. - 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 = Vec.from_str(flag['dir', '0 0 -1']).rotate_by_str( inst['angles', '0 0 0'] ) if utils.conv_bool(flag['gridpos', '0']): 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 = utils.conv_bool(flag['RemoveBrush', False], False) des_type = flag['type', 'any'].casefold() brush = SOLIDS.get(pos.as_tuple(), None) if brush is None or brush.normal != norm: br_type = 'none' else: br_type = str(brush.color) if should_remove: VMF.remove_brush( brush.solid, ) if result_var: inst.fixup[result_var] = br_type if des_type == 'any' and br_type != 'none': return True return des_type == br_type
def res_unst_scaffold_setup(res): 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": utils.conv_bool(block["isPiston", "0"]), "rotate_logic": utils.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(self): for val in true_strings: self.assertTrue(utils.conv_bool(val)) for val in false_strings: self.assertFalse(utils.conv_bool(val)) # Check that bools pass through self.assertTrue(utils.conv_bool(True)) self.assertFalse(utils.conv_bool(False)) # None passes through the default for val in def_vals: self.assertIs(utils.conv_bool(None, val), val)
def parse(cls, data): name = data.info['name'] unstyled = utils.conv_bool(data.info['unstyled', '0']) default = utils.conv_bool(data.info['enabled', '0']) styles = [ prop.value for prop in data.info.find_all('Style') ] return cls( data.id, name, styles, unstyled=unstyled, default=default, )
def parse(cls, data): name = data.info['name'] unstyled = utils.conv_bool(data.info['unstyled', '0']) default = utils.conv_bool(data.info['enabled', '0']) styles = [prop.value for prop in data.info.find_all('Style')] desc = '\n'.join(prop.value for prop in data.info.find_all('description')) return cls( data.id, name, styles, unstyled=unstyled, default=default, desc=desc, )
def flag_angles(inst, flag): """Check that a instance is pointed in a 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 = utils.conv_bool(flag["allow_inverse", "0"]) else: targ_angle = flag.value from_dir = Vec(0, 0, 1) allow_inverse = False if angle == targ_angle: return True # Check for exact match 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 angle = Vec.from_str(angle, 0, 0, 0) inst_normal = from_dir.rotate(angle.x, angle.y, angle.z) 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_add_global_inst(_, res): """Add one instance in a location. Once this is executed, it will be ignored thereafter. """ if res.value is not None: if utils.conv_bool(res["allow_multiple", "0"]) or res["file"] not in GLOBAL_INSTANCES: # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = VLib.Entity( 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() VMF.add_ent(new_inst) return True # Remove this result
def res_rand_vec(inst, res): """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 = utils.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 = utils.conv_float(res['max_' + axis, 0.0]) min_val = utils.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_global_inst(_, res): """Add one instance in a location. Once this is executed, it will be ignored thereafter. """ if res.value is not None: if (res['file'] not in GLOBAL_INSTANCES or utils.conv_bool(res['allow_multiple', '0'], True)): # 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 = VLib.Entity(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.append(res['file']) if new_inst['targetname'] == '': new_inst['targetname'] = "inst_" new_inst.make_unique() VMF.add_ent(new_inst) res.value = None # Disable this
def res_replace_instance(inst: VLib.Entity, res): """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 utils.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 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 = utils.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, res): """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 = utils.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 = utils.conv_float(res["max_" + axis, 0.0]) min_val = utils.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_cust_output(inst, res): """Add an additional output to the instance with any values. Always points to the targeted item. """ over_name = '@' + inst['targetname'] + '_indicator' for toggle in VMF.by_class['func_instance']: if toggle.fixup['indicator_name', ''] == over_name: toggle_name = toggle['targetname'] break else: toggle_name = '' # we want to ignore the toggle instance, if it exists # Make this a set to ignore repeated targetnames targets = {o.target for o in inst.outputs if o.target != toggle_name} kill_signs = utils.conv_bool(res["remIndSign", '0'], False) dec_con_count = utils.conv_bool(res["decConCount", '0'], False) targ_conditions = list(res.find_all("targCondition")) pan_files = resolve_inst('[indPan]') if kill_signs or dec_con_count or targ_conditions: for con_inst in VMF.by_class['func_instance']: if con_inst['targetname'] in targets: if kill_signs and con_inst in pan_files: VMF.remove_ent(con_inst) if targ_conditions: for cond in targ_conditions: cond.value.test(con_inst) if dec_con_count and 'connectioncount' in con_inst.fixup: # decrease ConnectionCount on the ents, # so they can still process normal inputs try: val = int(con_inst.fixup['connectioncount']) con_inst.fixup['connectioncount'] = str(val-1) except ValueError: # skip if it's invalid utils.con_log( con_inst['targetname'] + ' has invalid ConnectionCount!' ) for targ in targets: for out in res.find_all('addOut'): add_output(inst, out, targ)
def parse(cls, data): """Parse a style definition.""" info = data.info selitem_data = get_selitem_data(info) base = info['base', ''] has_video = utils.conv_bool(info['has_video', '1']) sugg = info.find_key('suggested', []) sugg = ( sugg['quote', '<NONE>'], sugg['music', '<NONE>'], sugg['skybox', 'SKY_BLACK'], sugg['goo', 'GOO_NORM'], sugg['elev', '<NONE>'], ) corridors = info.find_key('corridors', []) corridors = { 'sp_entry': corridors.find_key('sp_entry', []), 'sp_exit': corridors.find_key('sp_exit', []), 'coop': corridors.find_key('coop', []), } short_name = selitem_data.short_name or None if base == '': base = None folder = 'styles/' + info['folder'] config = folder + '/vbsp_config.cfg' with data.zip_file.open(folder + '/items.txt', 'r') as item_data: items = Property.parse( item_data, data.pak_id+':'+folder+'/items.txt' ) try: with data.zip_file.open(config, 'r') as vbsp_config: vbsp = Property.parse( vbsp_config, data.pak_id+':'+config, ) except KeyError: vbsp = None return cls( style_id=data.id, name=selitem_data.name, author=selitem_data.auth, desc=selitem_data.desc, icon=selitem_data.icon, editor=items, config=vbsp, base_style=base, short_name=short_name, suggested=sugg, has_video=has_video, corridor_names=corridors, )
def flag_is_preview(_, flag): """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. """ import vbsp return vbsp.IS_PREVIEW == utils.conv_bool(flag.value, False)
def parse(cls, data): """Parse an item definition.""" versions = {} def_version = None folders = {} needs_unlock = utils.conv_bool(data.info['needsUnlock', '0']) for ver in data.info.find_all('version'): vals = { 'name': ver['name', 'Regular'], 'id': ver['ID', 'VER_DEFAULT'], 'is_wip': utils.conv_bool(ver['wip', '0']), 'is_dep': utils.conv_bool(ver['deprecated', '0']), 'styles': {}, 'def_style': None, } for sty_list in ver.find_all('styles'): for sty in sty_list: if vals['def_style'] is None: vals['def_style'] = sty.value vals['styles'][sty.real_name] = sty.value folders[sty.value] = True versions[vals['id']] = vals if def_version is None: def_version = vals parse_item_folder(folders, data.zip_file) for ver in versions.values(): if ver['def_style'] in folders: ver['def_style'] = folders[ver['def_style']] for sty, fold in ver['styles'].items(): ver['styles'][sty] = folders[fold] if not versions: raise ValueError('Item "' + data.id + '" has no versions!') return cls(data.id, versions, def_version, needs_unlock)
def parse(cls, data): name = data.info['name'] unstyled = utils.conv_bool(data.info['unstyled', '0']) default = utils.conv_bool(data.info['enabled', '0']) styles = [ prop.value for prop in data.info.find_all('Style') ] desc = '\n'.join( prop.value for prop in data.info.find_all('description') ) return cls( data.id, name, styles, unstyled=unstyled, default=default, desc=desc, )
def parse(cls, data): """Parse a style definition.""" info = data.info selitem_data = get_selitem_data(info) base = info['base', ''] has_video = utils.conv_bool(info['has_video', '1']) sugg = info.find_key('suggested', []) sugg = ( sugg['quote', '<NONE>'], sugg['music', '<NONE>'], sugg['skybox', 'SKY_BLACK'], sugg['goo', 'GOO_NORM'], sugg['elev', '<NONE>'], ) corridors = info.find_key('corridors', []) corridors = { 'sp_entry': corridors.find_key('sp_entry', []), 'sp_exit': corridors.find_key('sp_exit', []), 'coop': corridors.find_key('coop', []), } if base == '': base = None folder = 'styles/' + info['folder'] config = folder + '/vbsp_config.cfg' with data.zip_file.open(folder + '/items.txt', 'r') as item_data: items = Property.parse(item_data, data.pak_id + ':' + folder + '/items.txt') try: with data.zip_file.open(config, 'r') as vbsp_config: vbsp = Property.parse( vbsp_config, data.pak_id + ':' + config, ) except KeyError: vbsp = None return cls( style_id=data.id, selitem_data=selitem_data, editor=items, config=vbsp, base_style=base, suggested=sugg, has_video=has_video, corridor_names=corridors, )
def res_change_inputs_setup(res: Property): vals = {} for prop in res: out_key = VLib.Output.parse_name(prop.real_name) if prop.has_children(): vals[out_key] = ( prop['inst_in', None], prop['input'], prop['params', ''], utils.conv_float(prop['delay', 0.0]), 1 if utils.conv_bool(prop['only_once', '0']) else -1, ) else: vals[out_key] = None return vals
def res_add_overlay_inst(inst, res): """Add another instance on top of this one.""" print("adding overlay", res["file"]) overlay_inst = VMF.create_ent( classname="func_instance", targetname=inst["targetname", ""], file=resolve_inst(res["file", ""])[0], angles=inst["angles", "0 0 0"], origin=inst["origin"], fixup_style=res["fixup_style", "0"], ) if utils.conv_bool(res["copy_fixup", "1"]): # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value
def res_vactube_setup(res): 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': utils.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 inst_configs[resolve_inst(file)[0]] = conf, utils.conv_int(size, 1) return group
def res_cust_output_setup(res): conds = [ Condition.parse(sub_res) for sub_res in res if sub_res.name == 'targcondition' ] outputs = list(res.find_all('addOut')) dec_con_count = utils.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 = VLib.Output.parse_name(res['sign_activate', '']) sign_deact = VLib.Output.parse_name(res['sign_deactivate', '']) return outputs, dec_con_count, conds, sign_type, sign_act, sign_deact
def flag_angles(inst, flag): """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 - 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 = utils.conv_bool(flag['allow_inverse', '0']) else: targ_angle = flag.value from_dir = Vec(0, 0, 1) allow_inverse = False if angle == targ_angle: return True # Check for exact match 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 make_static_pist(ent, res): """Convert a regular piston into a static version. This is done to save entities and improve lighting.""" bottom_pos = ent.fixup["bottom_level", "-1"] if ent.fixup["connectioncount", "0"] != "0" or ent.fixup["disable_autodrop", "0"] != "0": # can it move? if int(bottom_pos) > 0: # The piston doesn't go fully down, use alt instances. val = res.value["bottom_" + bottom_pos] if val: # Only if defined ent["file"] = val else: # we are static val = res.value[ "static_" + (ent.fixup["top_level", "1"] if utils.conv_bool(ent.fixup["start_up"], False) else bottom_pos) ] if val: ent["file"] = val
def res_hollow_brush(inst, res): """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=utils.conv_bool(res['RemoveFace', False]) )
def res_add_output_setup(res): output = res['output'] input_name = res['input'] inst_in = res['inst_out', ''] inst_out = res['inst_out', ''] targ = res['target'] only_once = utils.conv_bool(res['only_once', None]) times = 1 if only_once else utils.conv_int(res['times', None], -1) delay = utils.conv_float(res['delay', '0.0']) parm = res['parm', ''] return ( output, targ, input_name, parm, delay, times, inst_in, inst_out, )
def res_rand_num(inst, res): """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 = utils.conv_bool(res['decimal']) max_val = utils.conv_float(res['max', 1.0]) min_val = utils.conv_float(res['min', 0.0]) 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 inst.fixup[var] = str(func(min_val, max_val))
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. """ with zip_file.open(path + '.p2c') as file: props = Property.parse(file, path) props = props.find_key('portal2_puzzle', []) title = props['title', None] if title is None: title = '<' + path.rsplit('/', 1)[-1] + '.p2c>' return cls( path=path, zip_file = zip_file, title=title, desc=props['description', '...'], is_coop=utils.conv_bool(props['coop', '0']), create_time=Date(props['timestamp_created', '']), mod_time=Date(props['timestamp_modified', '']), )
def res_add_global_inst(_, res): """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 utils.conv_bool(res["allow_multiple", "0"]) or res["file"] not in GLOBAL_INSTANCES: # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = VLib.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 res_translate_inst(inst, res): """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 utils.conv_bool(inst.fixup['$start_up']) else '<piston_bottom>' ) if folded_val == '<piston_top>': val = Vec(z=128 * utils.conv_int(inst.fixup['$top_level', '1'], 1)) elif folded_val == '<piston_bottom>': val = Vec(z=128 * utils.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 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. 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. EncodedFile( file, data_encoding='utf-8', errors='replace', ), path, ) props = props.find_key('portal2_puzzle', []) title = props['title', None] if title is None: title = '<' + path.rsplit('/', 1)[-1] + '.p2c>' return cls( path=path, zip_file=zip_file, title=title, desc=props['description', '...'], is_coop=utils.conv_bool(props['coop', '0']), create_time=Date(props['timestamp_created', '']), mod_time=Date(props['timestamp_modified', '']), )
def show_window(used_props, parent, item_name): global propList, is_open, block_sound, last_angle propList = [key.casefold() for key in used_props] is_open = True spec_row = 1 start_up = utils.conv_bool(used_props.get('startup', '0')) values['startup'] = start_up for prop, value in used_props.items(): if prop in PROP_TYPES and value is not None: prop_type = PROP_TYPES[prop][0] if prop_type == 'checkbox': values[prop].set(utils.conv_bool(value)) elif prop_type == 'railLift': values[prop].set(utils.conv_bool(value)) save_rail(prop) elif prop_type == 'gelType': values[prop].set(value) elif prop_type == 'panAngle': last_angle = value[5:7] values[prop].set(last_angle) out_values[prop] = value elif prop_type == 'pistPlat': 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 == 'timerDel': try: values[prop] = int(value) widgets[prop].set(values[prop]) except ValueError: pass else: values[prop] = value 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 block_sound = False win.after(50, reset_sfx) widgets['titleLabel'].configure(text='Settings for "' + item_name + '"') win.title('BEE2 - ' + item_name) win.transient(master=parent) win.deiconify() win.lift(parent) win.grab_set() win.geometry('+' + str(parent.winfo_rootx() - 30) + '+' + str(parent.winfo_rooty() - win.winfo_reqheight() - 30))
def main(argv): utils.con_log('BEE2 VRAD hook started!') args = " ".join(argv) fast_args = argv[1:] full_args = argv[1:] path = argv[-1] # The path is the last argument to vrad fast_args[-1] = os.path.normpath(path) utils.con_log("Map path is " + path) if path == "": raise Exception("No map passed!") load_config() for a in fast_args[:]: if a.casefold() in ( "-both", "-final", "-staticproplighting", "-staticproppolys", "-textureshadows", ): # remove final parameters from the modified arguments fast_args.remove(a) elif a in ('-force_peti', '-force_hammer', '-no_pack'): # we need to strip these out, otherwise VBSP will get confused fast_args.remove(a) full_args.remove(a) fast_args = ['-bounce', '2', '-noextra'] + fast_args # Fast args: -bounce 2 -noextra -game $gamedir $path\$file # Final args: -both -final -staticproplighting -StaticPropPolys # -textureshadows -game $gamedir $path\$file if not path.endswith(".bsp"): path += ".bsp" if '-force_peti' in args or '-force_hammer' in args: # we have override command! if '-force_peti' in args: utils.con_log('OVERRIDE: Applying cheap lighting!') is_peti = True else: utils.con_log('OVERRIDE: Preserving args!') is_peti = False else: # If we don't get the special -force args, check for the name # equalling preview to determine if we should convert # If that is false, check the config file to see what was # specified there. is_peti = (os.path.basename(path) == "preview.bsp" or utils.conv_bool(CONF['force_full'], False)) mod_screenshots() if is_peti: utils.con_log("Forcing Cheap Lighting!") run_vrad(fast_args) else: utils.con_log("Hammer map detected! Not forcing cheap lighting..") run_vrad(full_args) if '-no_pack' not in args: pack_content(path) else: utils.con_log("No items to pack!") utils.con_log("BEE2 VRAD hook finished!")
def mod_screenshots(): """Modify the map's screenshot.""" mod_type = CONF['screenshot_type', 'PETI'].lower() if mod_type == 'cust': utils.con_log('Using custom screenshot!') scr_loc = CONF['screenshot', ''] elif mod_type == 'auto': utils.con_log('Using automatic screenshot!') scr_loc = None # The automatic screenshots are found at this location: auto_path = os.path.join('..', '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. screens = [ os.path.join(auto_path, path) for path in os.listdir(auto_path) ] screens.sort( key=os.path.getmtime, reverse=True, # Go from most recent to least ) playtested = False for scr_shot in screens: utils.con_log(scr_shot) 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: utils.con_log('Screenshot "{scr}" too old ({diff!s})'.format( scr=scr_shot, diff=diff)) continue # If we got here, it's a good screenshot! utils.con_log('Chosen "{}"'.format(scr_shot)) utils.con_log('Map Playtested:', playtested) scr_loc = scr_shot break else: # If we get to the end, we failed to find an automatic # screenshot! utils.con_log('No Auto Screenshot found!') mod_type = 'peti' # Suppress the "None not found" error if utils.conv_bool(CONF['clean_screenshots', '0']): utils.con_log('Cleaning up screenshots...') # Clean up this folder - otherwise users will get thousands of # pics in there! for screen in screens: if screen != scr_loc: os.remove(screen) utils.con_log('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(): utils.con_log('Replacing "{}"...'.format(screen)) # Allow us to edit the file... unset_readonly(screen) shutil.copy(scr_loc, screen) # Make the screenshot readonly, so P2 can't replace it. # Then it'll use our own set_readonly(screen) else: if mod_type != 'peti': # Error if the screenshot doesn't exist utils.con_log('"{}" not found!'.format(scr_loc)) utils.con_log('Using PeTI screenshot!') for screen in find_screenshots(): # Make the screenshot writeable, so P2 will replace it utils.con_log('Making "{}" replaceable...'.format(screen)) unset_readonly(screen)