def set_visible(is_visible: bool): """Show or hide the window.""" if is_visible: window.deiconify() else: window.withdraw() GEN_OPTS['Debug']['show_log_win'] = srctools.bool_as_int(is_visible)
def export(exp_data: ExportData) -> None: """Export the quotepack.""" if exp_data.selected is None: return # No quote pack! try: voice = QuotePack.by_id(exp_data.selected) # type: QuotePack except KeyError: raise Exception("Selected voice ({}) doesn't exist?".format( exp_data.selected)) from None vbsp_config = exp_data.vbsp_conf # type: Property # We want to strip 'trans' sections from the voice pack, since # they're not useful. for prop in voice.config: if prop.name == 'quotes': vbsp_config.append(QuotePack.strip_quote_data(prop)) else: vbsp_config.append(prop.copy()) # Set values in vbsp_config, so flags can determine which voiceline # is selected. options = vbsp_config.ensure_exists('Options') options['voice_pack'] = voice.id options['voice_char'] = ','.join(voice.chars) if voice.cave_skin is not None: options['cave_port_skin'] = str(voice.cave_skin) if voice.studio is not None: options['voice_studio_inst'] = voice.studio options['voice_studio_actor'] = voice.studio_actor options['voice_studio_inter_chance'] = str(voice.inter_chance) options['voice_studio_cam_loc'] = voice.cam_loc.join(' ') options['voice_studio_cam_pitch'] = str(voice.cam_pitch) options['voice_studio_cam_yaw'] = str(voice.cam_yaw) options['voice_studio_should_shoot'] = srctools.bool_as_int( voice.turret_hate) # Copy the config files for this voiceline.. for prefix, pretty in [('', 'normal'), ('mid_', 'MidChamber'), ('resp_', 'Responses')]: path = utils.conf_location('config/voice/') / (prefix.upper() + voice.id + '.cfg') LOGGER.info('Voice conf path: {}', path) if path.is_file(): shutil.copy( str(path), exp_data.game.abs_path( 'bin/bee2/{}voice.cfg'.format(prefix))) LOGGER.info('Written "{}voice.cfg"', prefix) else: LOGGER.info('No {} voice config!', pretty)
def make_static_pist(vmf: srctools.VMF, ent: Entity, res: Property): """Convert a regular piston into a static version. This is done to save entities and improve lighting. If changed to static pistons, the $bottom and $top level become equal. Instances: Bottom_1/2/3: Moving piston with the given $bottom_level Logic_0/1/2/3: Additional logic instance for the given $bottom_level Static_0/1/2/3/4: A static piston at the given height. Alternatively, specify all instances via editoritems, by setting the value to the item ID optionally followed by a :prefix. """ bottom_pos = ent.fixup.int('bottom_level', 0) if (ent.fixup['connectioncount', '0'] != "0" or ent.fixup['disable_autodrop', '0'] != "0"): # can it move? ent.fixup['$is_static'] = True # Use instances based on the height of the bottom position. val = res.value['bottom_' + str(bottom_pos)] if val: # Only if defined ent['file'] = val logic_file = res.value['logic_' + str(bottom_pos)] if logic_file: # Overlay an additional logic file on top of the original # piston. This allows easily splitting the piston logic # from the styled components logic_ent = ent.copy() logic_ent['file'] = logic_file vmf.add_ent(logic_ent) # If no connections are present, set the 'enable' value in # the logic to True so the piston can function logic_ent.fixup['manager_a'] = srctools.bool_as_int( ent.fixup['connectioncount', '0'] == '0') else: # we are static ent.fixup['$is_static'] = False if ent.fixup.bool('start_up'): pos = bottom_pos = ent.fixup.int('top_level', 1) else: pos = bottom_pos ent.fixup['top_level'] = ent.fixup['bottom_level'] = pos val = res.value['static_' + str(pos)] if val: ent['file'] = val # Add in the grating for the bottom as an overlay. # It's low to fit the piston at minimum, or higher if needed. grate = res.value['grate_high' if bottom_pos > 0 else 'grate_low'] if grate: grate_ent = ent.copy() grate_ent['file'] = grate vmf.add_ent(grate_ent)
def export(exp_data: ExportData) -> None: """Export style var selections into the config. The .selected attribute is a dict mapping ids to the boolean value. """ # Add the StyleVars block, containing each style_var. exp_data.vbsp_conf.append( Property('StyleVars', [ Property(key, bool_as_int(val)) for key, val in exp_data.selected.items() ]))
def save_conf(self) -> None: """Write configuration to the config file.""" if not self.can_save: return opt_block = GEN_OPTS['win_state'] opt_block[self.win_name + '_visible'] = srctools.bool_as_int(self.visible.get()) opt_block[self.win_name + '_x'] = str(self.relX) opt_block[self.win_name + '_y'] = str(self.relY) if self.can_resize_x: opt_block[self.win_name + '_width'] = str(self.winfo_width()) if self.can_resize_y: opt_block[self.win_name + '_height'] = str(self.winfo_height())
def save(self, ignore_readonly=False): """Save the palette file into the specified location. If ignore_readonly is true, this will ignore the `prevent_overwrite` property of the palette (allowing resaving those properties over old versions). Otherwise those palettes always create a new file. """ LOGGER.info('Saving "{}"!', self.name) props = Property(None, [ Property('Name', self.name), Property('TransName', self.trans_name), Property('ReadOnly', srctools.bool_as_int(self.prevent_overwrite)), Property('Items', [ Property(item_id, str(subitem)) for item_id, subitem in self.pos ]) ]) # If default, don't include in the palette file. # Remove the translated name, in case it's not going to write # properly to the file. if self.trans_name: props['Name'] = '' else: del props['TransName'] if not self.prevent_overwrite: del props['ReadOnly'] # We need to write a new file, determine a valid path. # Use a hash to ensure it's a valid path (without '-' if negative) # If a conflict occurs, add ' ' and hash again to get a different # value. if self.filename is None or (self.prevent_overwrite and not ignore_readonly): hash_src = self.name while True: hash_filename = str(abs(hash(hash_src))) + PAL_EXT if os.path.isfile(hash_filename): # Add a random character to iterate the hash. hash_src += chr(random.randrange(0x10ffff)) else: file = open(os.path.join(PAL_DIR, hash_filename), 'w', encoding='utf8') self.filename = os.path.join(PAL_DIR, hash_filename) break else: file = open(os.path.join(PAL_DIR, self.filename), 'w', encoding='utf8') with file: for line in props.export(): file.write(line)
def save_conf(self): if self.can_save: self.config_file['win_state'][ self.win_name + '_visible'] = srctools.bool_as_int(self.visible) self.config_file['win_state'][self.win_name + '_x'] = str(self.relX) self.config_file['win_state'][self.win_name + '_y'] = str(self.relY) if self.can_resize_x: self.config_file['win_state'][ self.win_name + '_width'] = str(self.winfo_width()) if self.can_resize_y: self.config_file['win_state'][ self.win_name + '_height'] = str(self.winfo_height())
def save_pist(key, val): if widgets['toplevel'].get() == widgets['bottomlevel'].get(): # user moved them to match, switch the other one around sound.fx_blockable('swap') widgets['toplevel' if key == 'bottomlevel' else 'bottomlevel'].set( values[key]) else: sound.fx_blockable('move') start_pos = widgets['toplevel'].get() end_pos = widgets['bottomlevel'].get() values['toplevel'] = start_pos values['bottomlevel'] = end_pos values['startup'] = srctools.bool_as_int(start_pos > end_pos) out_values['toplevel'] = str(max(start_pos, end_pos)) out_values['bottomlevel'] = str(min(start_pos, end_pos))
def save_pist(key, val): if widgets['toplevel'].get() == widgets['bottomlevel'].get(): # user moved them to match, switch the other one around sound.fx_blockable('swap') widgets[ 'toplevel' if key == 'bottomlevel' else 'bottomlevel' ].set(values[key]) else: sound.fx_blockable('move') start_pos = widgets['toplevel'].get() end_pos = widgets['bottomlevel'].get() values['toplevel'] = start_pos values['bottomlevel'] = end_pos values['startup'] = srctools.bool_as_int(start_pos > end_pos) out_values['toplevel'] = str(max(start_pos, end_pos)) out_values['bottomlevel'] = str(min(start_pos, end_pos))
def save_pist(key, val): """The top and bottom positions are closely interrelated.""" global enable_pist_callback if not enable_pist_callback: return try: top_wid: ttk.Scale = widgets['toplevel'] btm_wid: ttk.Scale = widgets['bottomlevel'] except KeyError: return # Both don't exist yet. # The ttk Scale widget doesn't snap to integers, so we need to do that. prev_top = top_wid.get() new_top = round(prev_top) prev_btm = btm_wid.get() new_btm = round(prev_btm) enable_pist_callback = False top_wid.set(new_top) btm_wid.set(new_btm) enable_pist_callback = True if top_wid.get() == btm_wid.get(): # user moved them to match, switch the other one around sound.fx_blockable('swap') (top_wid if key == 'bottomlevel' else btm_wid).set(values[key]) elif prev_top != new_top or prev_btm != new_btm: # Only play when we've actually changed. sound.fx_blockable('move') values['toplevel'] = start_pos = top_wid.get() values['bottomlevel'] = end_pos = btm_wid.get() values['startup'] = srctools.bool_as_int(start_pos > end_pos) out_values['toplevel'] = str(max(start_pos, end_pos)) out_values['bottomlevel'] = str(min(start_pos, end_pos))
def check_callback(): GEN_OPTS['General']['enable_auto_backup'] = srctools.bool_as_int( check_var.get())
def enabled(self, value: bool) -> None: """Enable or disable the package.""" if self.id.casefold() == CLEAN_PACKAGE: raise ValueError('The Clean Style package cannot be disabled!') PACK_CONFIG[self.id]['Enabled'] = srctools.bool_as_int(value)
def check_toggled(var, config_section, quote_id): """Update the config file to match the checkbox.""" config_section[quote_id] = srctools.bool_as_int(var.get())
def test_bool_as_int(): """Test result of srctools.bool_as_int.""" for val in true_vals: assert srctools.bool_as_int(val) == '1', repr(val) for val in false_vals: assert srctools.bool_as_int(val) == '0', repr(val)
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property): """Add an Aperture Tag Paint Gun activation fizzler. These fizzlers are created via signs, and work very specially. MUST be priority -100 so it runs before fizzlers! """ import vbsp if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizzler = None # Look for the fizzler instance we want to replace for targetname in inst.output_targets(): try: fizzler = FIZZLERS[targetname] except KeyError: # Not a fizzler. # It's an indicator toggle, remove it and the antline to clean up. for ent in vmf.by_target[targetname]: remove_ant_toggle(ent) inst.outputs.clear() # Remove the outputs now, they're not valid anyway. if fizzler is None: # No fizzler - remove this sign inst.remove() return if fizzler.fizz_type.id == 'TAG_FIZZ_ID': LOGGER.warning('Two tag signs attached to one fizzler...') inst.remove() return # Swap to the special Tag Fizzler type. fizzler.fizz_type = FIZZ_TYPES[TAG_FIZZ_ID] # The distance from origin the double signs are seperated by. sign_offset = res.int('signoffset', 16) sign_loc = ( # The actual location of the sign - on the wall Vec.from_str(inst['origin']) + Vec(0, 0, -64).rotate_by_str(inst['angles']) ) # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = ( not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled ) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_angle = srctools.parse_vec_str(inst['angles']) inst_normal = Vec(0, 0, 1).rotate(*inst_angle) loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = res['frame_double'] # On a wall, and pointing vertically if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0).rotate(*inst_angle) blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = res['frame_single'] # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. # Grab the data saved earlier in res_find_potential_tag_fizzlers() axis, side_min, side_max, normal = calc_fizzler_orient(fizzler) # The Z-axis fizzler (horizontal) must be treated differently. if axis == 'z': # For z-axis, just compare to the center point. # The values are really x, y, z, not what they're named. sign_dir = sign_floor_loc - (side_min, side_max, normal) else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) other_axis = 'x' if axis == 'y' else 'y' if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. Use ** to swap x/y arguments # appropriately. The closest side is the one with the # smallest magnitude. sign_dir = min( sign_floor_loc - Vec.with_axes( axis,side_min, other_axis, normal, ), sign_floor_loc - Vec.with_axes( axis, side_max, other_axis, normal, ), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec() sign_dir[other_axis] = sign_floor_loc[other_axis] - normal sign_angle = math.degrees( math.atan2(sign_dir.y, sign_dir.x) ) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_angle = (sign_angle + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_angle = int(sign_angle + 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_angle) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_angle) else: # On a wall, face upright sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = 'blue_sign' if blue_enabled else 'blue_off_sign' if disable_other else None oran_sign = 'oran_sign' if oran_enabled else 'oran_off_sign' if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=res[blue_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=res[oran_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... # Subtract the sign from the list of connections, but don't go below # zero fizzler.base_inst.fixup['$connectioncount'] = str(max( 0, srctools.conv_int(fizzler.base_inst.fixup['$connectioncount', ''], 0) - 1 )) # Find the direction the fizzler normal is. # Signs will associate with the given side! bbox_min, bbox_max = fizzler.emitters[0] fizz_field_axis = (bbox_max-bbox_min).norm() fizz_norm_axis = fizzler.normal().axis() sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2 # Figure out what the sides will set values to... pos_blue = False pos_oran = False neg_blue = False neg_oran = False if sign_loc[fizz_norm_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled # If it activates the paint gun, use different textures fizzler.tag_on_pos = pos_blue or pos_oran fizzler.tag_on_neg = neg_blue or neg_oran # Now make the trigger ents. We special-case these since they need to swap # depending on the sign config and position. if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers pos_trig = vmf.create_ent( classname='trigger_playerteam', ) neg_trig = vmf.create_ent( classname='trigger_playerteam', ) output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent( classname='trigger_multiple', ) neg_trig = vmf.create_ent( classname='trigger_multiple', spawnflags='1', ) output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizzler.base_inst['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = local_name(fizzler.base_inst, 'trig_pos') neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_neg') pos_trig.outputs = [ Output(output, neg_trig, 'Enable'), Output(output, pos_trig, 'Disable'), ] neg_trig.outputs = [ Output(output, pos_trig, 'Enable'), Output(output, neg_trig, 'Disable'), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_blue), )) pos_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_blue), )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_off') neg_trig.outputs.clear() neg_trig.add_out(Output( output, '@BlueIsEnabled', 'SetValue', param='0' )) neg_trig.add_out(Output( output, '@OrangeIsEnabled', 'SetValue', param='0' )) # Make the triggers. for bbox_min, bbox_max in fizzler.emitters: bbox_min = bbox_min.copy() - 64 * fizzler.up_axis bbox_max = bbox_max.copy() + 64 * fizzler.up_axis # The triggers are 8 units thick, with a 32-unit gap in the middle neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_norm_axis] -= 24 neg_max[fizz_norm_axis] -= 16 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_norm_axis] += 16 pos_max[fizz_norm_axis] += 24 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, )
def check_callback(): GEN_OPTS['General']['enable_auto_backup'] = srctools.bool_as_int( check_var.get() )
def save_opt(): """Save the checkbox's values.""" GEN_OPTS[section][item] = srctools.bool_as_int( var.get() )
def _export_value(value: bool) -> str: return bool_as_int(value)
def init(cback): global callback, labels, win, is_open callback = cback is_open = False win = Toplevel(TK_ROOT) win.title("BEE2") win.resizable(False, False) tk_tools.set_window_icon(win) win.protocol("WM_DELETE_WINDOW", exit_win) win.transient(TK_ROOT) win.withdraw() if utils.MAC: # Switch to use the 'modal' window style on Mac. TK_ROOT.call( '::tk::unsupported::MacWindowStyle', 'style', win, 'moveableModal', '' ) frame = ttk.Frame(win, padding=10) frame.grid(row=0, column=0, sticky='NSEW') frame.rowconfigure(0, weight=1) frame.columnconfigure(0, weight=1) labels['noOptions'] = ttk.Label(frame, text=_('No Properties available!')) widgets['saveButton'] = ttk.Button(frame, text=_('Close'), command=exit_win) widgets['titleLabel'] = ttk.Label(frame, text='') widgets['titleLabel'].grid(columnspan=9) widgets['div_1'] = ttk.Separator(frame, orient="vertical") widgets['div_2'] = ttk.Separator(frame, orient="vertical") widgets['div_h'] = ttk.Separator(frame, orient="horizontal") for key, (prop_type, prop_name) in PROP_TYPES.items(): # Translate property names from Valve's files. if prop_name.startswith('PORTAL2_'): prop_name = gameMan.translate(prop_name) + ':' labels[key] = ttk.Label(frame, text=prop_name) if prop_type is PropTypes.CHECKBOX: values[key] = IntVar(value=DEFAULTS[key]) out_values[key] = srctools.bool_as_int(DEFAULTS[key]) widgets[key] = ttk.Checkbutton( frame, variable=values[key], command=func_partial(set_check, key), ) widgets[key].bind( '<Return>', func_partial( toggleCheck, key, values[key], ) ) elif prop_type is PropTypes.OSCILLATE: values[key] = IntVar(value=DEFAULTS[key]) out_values[key] = srctools.bool_as_int(DEFAULTS[key]) widgets[key] = ttk.Checkbutton( frame, variable=values[key], command=func_partial(save_rail, key), ) elif prop_type is PropTypes.PANEL: frm = ttk.Frame(frame) widgets[key] = frm values[key] = StringVar(value=DEFAULTS[key]) for pos, (angle, disp_angle) in enumerate(PANEL_ANGLES): ttk.Radiobutton( frm, variable=values[key], value=angle, text=gameMan.translate(disp_angle), command=func_partial(save_angle, key, angle), ).grid(row=0, column=pos) frm.columnconfigure(pos, weight=1) elif prop_type is PropTypes.GELS: frm = ttk.Frame(frame) widgets[key] = frm values[key] = IntVar(value=DEFAULTS[key]) for pos, text in enumerate(PAINT_OPTS): ttk.Radiobutton( frm, variable=values[key], value=pos, text=gameMan.translate(text), command=func_partial(save_paint, key, pos), ).grid(row=0, column=pos) frm.columnconfigure(pos, weight=1) out_values[key] = str(DEFAULTS[key]) elif prop_type is PropTypes.PISTON: widgets[key] = Scale( frame, from_=0, to=4, orient="horizontal", showvalue=False, command=func_partial(save_pist, key), ) values[key] = DEFAULTS[key] out_values[key] = str(DEFAULTS[key]) if ((key == 'toplevel' and DEFAULTS['startup']) or (key == 'bottomlevel' and not DEFAULTS['startup'])): widgets[key].set(max( DEFAULTS['toplevel'], DEFAULTS['bottomlevel'] )) if ((key == 'toplevel' and not DEFAULTS['startup']) or (key == 'bottomlevel' and DEFAULTS['startup'])): widgets[key].set(min( DEFAULTS['toplevel'], DEFAULTS['bottomlevel'])) elif prop_type is PropTypes.TIMER: widgets[key] = ttk.Scale( frame, from_=0, to=30, orient="horizontal", command=func_partial(save_tim, key), ) values[key] = DEFAULTS[key] values['startup'] = DEFAULTS['startup']
def res_faith_mods(inst: Entity, res: Property): """Modify the `trigger_catapult` that is created for `ItemFaithPlate` items. Values: - `raise_trig`: Raise or lower the `trigger_catapult`s by this amount. - `angled_targ`, `angled_in`: Instance entity and input for angled plates - `straight_targ`, `straight_in`: Instance entity and input for straight plates - `instvar`: A $replace value to set to either 'angled' or ' 'straight'. - `enabledVar`: A `$replace` value which will be copied to the main trigger's Start Disabled value (and inverted). - `trig_temp`: An ID for a template brush to add. This will be offset by the trigger's position (in the case of the `helper` trigger). """ # Get data about the trigger this instance uses for flinging fixup_var = res['instvar', ''] trig_enabled = res['enabledVar', None] trig_temp = res['trig_temp', ''] offset = srctools.conv_int(res['raise_trig', '0']) if offset: offset = Vec(0, 0, offset).rotate_by_str(inst['angles', '0 0 0']) else: offset = Vec() if trig_enabled is not None: trig_enabled = srctools.conv_bool(inst.fixup[trig_enabled]) else: trig_enabled = None for trig in vbsp.VMF.by_class['trigger_catapult']: if inst['targetname'] not in trig['targetname']: continue # Edit both the normal and the helper trigger.. trig_origin = trig['origin'] = Vec.from_str(trig['origin']) + offset if offset and not trig_temp: # No template, shift the current brushes. for solid in trig.solids: solid.translate(offset) elif trig_temp: trig.solids = template_brush.import_template( temp_name=trig_temp, origin=trig_origin, angles=Vec.from_str(inst['angles']), force_type=template_brush.TEMP_TYPES.world, ).world # Remove the trigger solids from worldspawn.. for solid in trig.solids: vbsp.VMF.remove_brush(solid) if trig_enabled is not None and 'helper' not in trig['targetname']: trig['startdisabled'] = srctools.bool_as_int(not trig_enabled) # Inspect the outputs to determine the type. # We also change them if desired, since that's not possible # otherwise. for out in trig.outputs: if out.inst_in == 'animate_angled_relay': # Instead of an instance: output, use local names. # This allows us to strip the proxy, as well as use # overlay instances. out.inst_in = None out.target = conditions.local_name( inst, res['angled_targ', 'animate_angled_relay']) out.input = res['angled_in', 'Trigger'] if fixup_var: inst.fixup[fixup_var] = 'angled' break # There's only one output we want to look for... elif out.inst_in == 'animate_straightup_relay': out.inst_in = None out.target = conditions.local_name( inst, res['straight_targ', 'animate_straightup_relay'], ) out.input = res['straight_in', 'Trigger'] if fixup_var: inst.fixup[fixup_var] = 'straight' break
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property): """Add an Aperture Tag Paint Gun activation fizzler. These fizzlers are created via signs, and work very specially. MUST be priority -100 so it runs before fizzlers! """ import vbsp if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizz_base = fizz_name = None # Look for the fizzler instance we want to replace for targetname in inst.output_targets(): if targetname in tag_fizzlers: fizz_name = targetname fizz_base = tag_fizzlers[targetname] del tag_fizzlers[targetname] # Don't let other signs mod this one! continue else: # It's an indicator toggle, remove it and the antline to clean up. LOGGER.warning('Toggle: {}', targetname) for ent in vmf.by_target[targetname]: remove_ant_toggle(ent) inst.outputs.clear() # Remove the outptuts now, they're not valid anyway. if fizz_base is None: # No fizzler - remove this sign inst.remove() return # The distance from origin the double signs are seperated by. sign_offset = res.int('signoffset', 16) sign_loc = ( # The actual location of the sign - on the wall Vec.from_str(inst['origin']) + Vec(0, 0, -64).rotate_by_str(inst['angles'])) # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = (not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_angle = srctools.parse_vec_str(inst['angles']) inst_normal = Vec(0, 0, 1).rotate(*inst_angle) loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = res['frame_double'] # On a wall, and pointing vertically if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0).rotate(*inst_angle) blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = res['frame_single'] # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. # Grab the data saved earlier in res_find_potential_tag_fizzlers() axis, side_min, side_max, normal = tag_fizzler_locs[fizz_name] # The Z-axis fizzler (horizontal) must be treated differently. if axis == 'z': # For z-axis, just compare to the center point. # The values are really x, y, z, not what they're named. sign_dir = sign_floor_loc - (side_min, side_max, normal) else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) other_axis = 'x' if axis == 'y' else 'y' if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. Use ** to swap x/y arguments # appropriately. The closest side is the one with the # smallest magnitude. vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_min', origin=sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), ) vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_max', origin=sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), ) sign_dir = min( sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec() sign_dir[other_axis] = sign_floor_loc[other_axis] - normal sign_angle = math.degrees(math.atan2(sign_dir.y, sign_dir.x)) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_angle = (sign_angle + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_angle = int(sign_angle + 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_angle) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_angle) else: # On a wall, face upright sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = 'blue_sign' if blue_enabled else 'blue_off_sign' if disable_other else None oran_sign = 'oran_sign' if oran_enabled else 'oran_off_sign' if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=res[blue_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=res[oran_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... fizz_brushes = list(vmf.by_class['trigger_portal_cleanser'] & vmf.by_target[fizz_name + '_brush']) if 'base_inst' in res: fizz_base['file'] = instanceLocs.resolve_one(res['base_inst'], error=True) fizz_base.outputs.clear() # Remove outputs, otherwise they break # branch_toggle entities # Subtract the sign from the list of connections, but don't go below # zero fizz_base.fixup['$connectioncount'] = str( max(0, srctools.conv_int(fizz_base.fixup['$connectioncount', ''], 0) - 1)) if 'model_inst' in res: model_inst = instanceLocs.resolve_one(res['model_inst'], error=True) for mdl_inst in vmf.by_class['func_instance']: if mdl_inst['targetname', ''].startswith(fizz_name + '_model'): mdl_inst['file'] = model_inst # Find the direction the fizzler front/back points - z=floor fizz # Signs will associate with the given side! bbox_min, bbox_max = fizz_brushes[0].get_bbox() for axis, val in zip('xyz', bbox_max - bbox_min): if val == 2: fizz_axis = axis sign_center = (bbox_min[axis] + bbox_max[axis]) / 2 break else: # A fizzler that's not 128*x*2? raise Exception('Invalid fizzler brush ({})!'.format(fizz_name)) # Figure out what the sides will set values to... pos_blue = False pos_oran = False neg_blue = False neg_oran = False if sign_loc[fizz_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled fizz_off_tex = { 'left': res['off_left'], 'center': res['off_center'], 'right': res['off_right'], 'short': res['off_short'], } fizz_on_tex = { 'left': res['on_left'], 'center': res['on_center'], 'right': res['on_right'], 'short': res['on_short'], } # If it activates the paint gun, use different textures if pos_blue or pos_oran: pos_tex = fizz_on_tex else: pos_tex = fizz_off_tex if neg_blue or neg_oran: neg_tex = fizz_on_tex else: neg_tex = fizz_off_tex if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers pos_trig = vmf.create_ent(classname='trigger_playerteam', ) neg_trig = vmf.create_ent(classname='trigger_playerteam', ) output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent(classname='trigger_multiple', ) neg_trig = vmf.create_ent( classname='trigger_multiple', spawnflags='1', ) output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizz_base['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = fizz_name + '-trig_pos' neg_trig['targetname'] = fizz_name + '-trig_neg' pos_trig.outputs = [ Output( output, fizz_name + '-trig_neg', 'Enable', ), Output( output, fizz_name + '-trig_pos', 'Disable', ), ] neg_trig.outputs = [ Output( output, fizz_name + '-trig_pos', 'Enable', ), Output( output, fizz_name + '-trig_neg', 'Disable', ), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_blue), )) pos_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_blue), )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = fizz_name + '-trig_off' neg_trig.outputs.clear() neg_trig.add_out( Output(output, '@BlueIsEnabled', 'SetValue', param='0')) neg_trig.add_out( Output(output, '@OrangeIsEnabled', 'SetValue', param='0')) for fizz_brush in fizz_brushes: # portal_cleanser ent, not solid! # Modify fizzler textures bbox_min, bbox_max = fizz_brush.get_bbox() for side in fizz_brush.sides(): norm = side.normal() if norm[fizz_axis] == 0: # Not the front/back: force nodraw # Otherwise the top/bottom will have the odd stripes # which won't match the sides side.mat = 'tools/toolsnodraw' continue if norm[fizz_axis] == 1: side.mat = pos_tex[vbsp.TEX_FIZZLER[side.mat.casefold()]] else: side.mat = neg_tex[vbsp.TEX_FIZZLER[side.mat.casefold()]] # The fizzler shouldn't kill cubes fizz_brush['spawnflags'] = '1' fizz_brush.outputs.append( Output( 'OnStartTouch', '@shake_global', 'StartShake', )) fizz_brush.outputs.append( Output( 'OnStartTouch', '@shake_global_sound', 'PlaySound', )) # The triggers are 8 units thick, 24 from the center # (-1 because fizzlers are 2 thick on each side). neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_axis] -= 23 neg_max[fizz_axis] -= 17 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_axis] += 17 pos_max[fizz_axis] += 23 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, )
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property): """Add an Aperture Tag Paint Gun activation fizzler. These fizzlers are created via signs, and work very specially. MUST be priority -100 so it runs before fizzlers! """ import vbsp if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizz_base = fizz_name = None # Look for the fizzler instance we want to replace for targetname in inst.output_targets(): if targetname in tag_fizzlers: fizz_name = targetname fizz_base = tag_fizzlers[targetname] del tag_fizzlers[targetname] # Don't let other signs mod this one! continue else: # It's an indicator toggle, remove it and the antline to clean up. LOGGER.warning('Toggle: {}', targetname) for ent in vmf.by_target[targetname]: remove_ant_toggle(ent) inst.outputs.clear() # Remove the outptuts now, they're not valid anyway. if fizz_base is None: # No fizzler - remove this sign inst.remove() return # The distance from origin the double signs are seperated by. sign_offset = res.int('signoffset', 16) sign_loc = ( # The actual location of the sign - on the wall Vec.from_str(inst['origin']) + Vec(0, 0, -64).rotate_by_str(inst['angles']) ) # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = ( not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled ) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_angle = srctools.parse_vec_str(inst['angles']) inst_normal = Vec(0, 0, 1).rotate(*inst_angle) loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = res['frame_double'] # On a wall, and pointing vertically if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0).rotate(*inst_angle) blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = res['frame_single'] # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. # Grab the data saved earlier in res_find_potential_tag_fizzlers() axis, side_min, side_max, normal = tag_fizzler_locs[fizz_name] # The Z-axis fizzler (horizontal) must be treated differently. if axis == 'z': # For z-axis, just compare to the center point. # The values are really x, y, z, not what they're named. sign_dir = sign_floor_loc - (side_min, side_max, normal) else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) other_axis = 'x' if axis == 'y' else 'y' if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. Use ** to swap x/y arguments # appropriately. The closest side is the one with the # smallest magnitude. vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_min', origin=sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), ) vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_max', origin=sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), ) sign_dir = min( sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec() sign_dir[other_axis] = sign_floor_loc[other_axis] - normal sign_angle = math.degrees( math.atan2(sign_dir.y, sign_dir.x) ) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_angle = (sign_angle + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_angle = int(sign_angle + 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_angle) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_angle) else: # On a wall, face upright sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = 'blue_sign' if blue_enabled else 'blue_off_sign' if disable_other else None oran_sign = 'oran_sign' if oran_enabled else 'oran_off_sign' if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=res[blue_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=res[oran_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... fizz_brushes = list( vmf.by_class['trigger_portal_cleanser'] & vmf.by_target[fizz_name + '_brush'] ) if 'base_inst' in res: fizz_base['file'] = resolve_inst(res['base_inst'])[0] fizz_base.outputs.clear() # Remove outputs, otherwise they break # branch_toggle entities # Subtract the sign from the list of connections, but don't go below # zero fizz_base.fixup['$connectioncount'] = str(max( 0, srctools.conv_int(fizz_base.fixup['$connectioncount', ''], 0) - 1 )) if 'model_inst' in res: model_inst = resolve_inst(res['model_inst'])[0] for mdl_inst in vmf.by_class['func_instance']: if mdl_inst['targetname', ''].startswith(fizz_name + '_model'): mdl_inst['file'] = model_inst # Find the direction the fizzler front/back points - z=floor fizz # Signs will associate with the given side! bbox_min, bbox_max = fizz_brushes[0].get_bbox() for axis, val in zip('xyz', bbox_max-bbox_min): if val == 2: fizz_axis = axis sign_center = (bbox_min[axis] + bbox_max[axis]) / 2 break else: # A fizzler that's not 128*x*2? raise Exception('Invalid fizzler brush ({})!'.format(fizz_name)) # Figure out what the sides will set values to... pos_blue = False pos_oran = False neg_blue = False neg_oran = False if sign_loc[fizz_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled fizz_off_tex = { 'left': res['off_left'], 'center': res['off_center'], 'right': res['off_right'], 'short': res['off_short'], } fizz_on_tex = { 'left': res['on_left'], 'center': res['on_center'], 'right': res['on_right'], 'short': res['on_short'], } # If it activates the paint gun, use different textures if pos_blue or pos_oran: pos_tex = fizz_on_tex else: pos_tex = fizz_off_tex if neg_blue or neg_oran: neg_tex = fizz_on_tex else: neg_tex = fizz_off_tex if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers pos_trig = vmf.create_ent( classname='trigger_playerteam', ) neg_trig = vmf.create_ent( classname='trigger_playerteam', ) output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent( classname='trigger_multiple', ) neg_trig = vmf.create_ent( classname='trigger_multiple', spawnflags='1', ) output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizz_base['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = fizz_name + '-trig_pos' neg_trig['targetname'] = fizz_name + '-trig_neg' pos_trig.outputs = [ Output( output, fizz_name + '-trig_neg', 'Enable', ), Output( output, fizz_name + '-trig_pos', 'Disable', ), ] neg_trig.outputs = [ Output( output, fizz_name + '-trig_pos', 'Enable', ), Output( output, fizz_name + '-trig_neg', 'Disable', ), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_blue), )) pos_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_blue), )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = fizz_name + '-trig' neg_trig.outputs.clear() neg_trig.add_out(Output( output, '@BlueIsEnabled', 'SetValue', param='0' )) neg_trig.add_out(Output( output, '@OrangeIsEnabled', 'SetValue', param='0' )) for fizz_brush in fizz_brushes: # portal_cleanser ent, not solid! # Modify fizzler textures bbox_min, bbox_max = fizz_brush.get_bbox() for side in fizz_brush.sides(): norm = side.normal() if norm[fizz_axis] == 0: # Not the front/back: force nodraw # Otherwise the top/bottom will have the odd stripes # which won't match the sides side.mat = 'tools/toolsnodraw' continue if norm[fizz_axis] == 1: side.mat = pos_tex[ vbsp.TEX_FIZZLER[ side.mat.casefold() ] ] else: side.mat = neg_tex[ vbsp.TEX_FIZZLER[ side.mat.casefold() ] ] # The fizzler shouldn't kill cubes fizz_brush['spawnflags'] = '1' fizz_brush.outputs.append(Output( 'OnStartTouch', '@shake_global', 'StartShake', )) fizz_brush.outputs.append(Output( 'OnStartTouch', '@shake_global_sound', 'PlaySound', )) # The triggers are 8 units thick, 24 from the center # (-1 because fizzlers are 2 thick on each side). neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_axis] -= 23 neg_max[fizz_axis] -= 17 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_axis] += 17 pos_max[fizz_axis] += 23 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, )
def save_opt(): """Save the checkbox's values.""" GEN_OPTS[section][item] = srctools.bool_as_int(var.get())
def init(cback): global callback, labels, win, is_open callback = cback is_open = False win = Toplevel(TK_ROOT) win.title("BEE2") win.resizable(False, False) tk_tools.set_window_icon(win) win.protocol("WM_DELETE_WINDOW", exit_win) win.transient(TK_ROOT) win.withdraw() if utils.MAC: # Switch to use the 'modal' window style on Mac. TK_ROOT.call('::tk::unsupported::MacWindowStyle', 'style', win, 'moveableModal', '') # Stop our init from triggering UI sounds. sound.block_fx() frame = ttk.Frame(win, padding=10) frame.grid(row=0, column=0, sticky='NSEW') frame.rowconfigure(0, weight=1) frame.columnconfigure(0, weight=1) labels['noOptions'] = ttk.Label(frame, text=_('No Properties available!')) widgets['saveButton'] = ttk.Button(frame, text=_('Close'), command=exit_win) widgets['titleLabel'] = ttk.Label(frame, text='') widgets['titleLabel'].grid(columnspan=9) widgets['div_1'] = ttk.Separator(frame, orient="vertical") widgets['div_2'] = ttk.Separator(frame, orient="vertical") widgets['div_h'] = ttk.Separator(frame, orient="horizontal") for key, (prop_type, prop_name) in PROP_TYPES.items(): # Translate property names from Valve's files. if prop_name.startswith('PORTAL2_'): prop_name = gameMan.translate(prop_name) + ':' labels[key] = ttk.Label(frame, text=prop_name) if prop_type is PropTypes.CHECKBOX: values[key] = IntVar(value=DEFAULTS[key]) out_values[key] = srctools.bool_as_int(DEFAULTS[key]) widgets[key] = ttk.Checkbutton( frame, variable=values[key], command=func_partial(set_check, key), ) widgets[key].bind('<Return>', func_partial( toggleCheck, key, values[key], )) elif prop_type is PropTypes.OSCILLATE: values[key] = IntVar(value=DEFAULTS[key]) out_values[key] = srctools.bool_as_int(DEFAULTS[key]) widgets[key] = ttk.Checkbutton( frame, variable=values[key], command=func_partial(save_rail, key), ) elif prop_type is PropTypes.PANEL: frm = ttk.Frame(frame) widgets[key] = frm values[key] = StringVar(value=DEFAULTS[key]) for pos, (angle, disp_angle) in enumerate(PANEL_ANGLES): ttk.Radiobutton( frm, variable=values[key], value=angle, text=gameMan.translate(disp_angle), command=func_partial(save_angle, key, angle), ).grid(row=0, column=pos) frm.columnconfigure(pos, weight=1) elif prop_type is PropTypes.GELS: frm = ttk.Frame(frame) widgets[key] = frm values[key] = IntVar(value=DEFAULTS[key]) for pos, text in enumerate(PAINT_OPTS): ttk.Radiobutton( frm, variable=values[key], value=pos, text=gameMan.translate(text), command=func_partial(save_paint, key, pos), ).grid(row=0, column=pos) frm.columnconfigure(pos, weight=1) out_values[key] = str(DEFAULTS[key]) elif prop_type is PropTypes.PISTON: widgets[key] = pist_scale = ttk.Scale( frame, from_=0, to=4, orient="horizontal", command=func_partial(save_pist, key), ) values[key] = DEFAULTS[key] out_values[key] = str(DEFAULTS[key]) if ((key == 'toplevel' and DEFAULTS['startup']) or (key == 'bottomlevel' and not DEFAULTS['startup'])): pist_scale.set( max(DEFAULTS['toplevel'], DEFAULTS['bottomlevel'])) if ((key == 'toplevel' and not DEFAULTS['startup']) or (key == 'bottomlevel' and DEFAULTS['startup'])): pist_scale.set( min(DEFAULTS['toplevel'], DEFAULTS['bottomlevel'])) elif prop_type is PropTypes.TIMER: widgets[key] = ttk.Scale( frame, from_=0, to=30, orient="horizontal", command=func_partial(save_tim, key), ) values[key] = DEFAULTS[key] values['startup'] = DEFAULTS['startup']
def export( self, style: packages.Style, selected_objects: dict, should_refresh=False, ) -> Tuple[bool, bool]: """Generate the editoritems.txt and vbsp_config. - If no backup is present, the original editoritems is backed up. - For each object type, run its .export() function with the given - item. - Styles are a special case. """ LOGGER.info('-' * 20) LOGGER.info('Exporting Items and Style for "{}"!', self.name) LOGGER.info('Style = {}', style.id) for obj, selected in selected_objects.items(): # Skip the massive dict in items if obj == 'Item': selected = selected[0] LOGGER.info('{} = {}', obj, selected) # VBSP, VRAD, editoritems export_screen.set_length('BACK', len(FILES_TO_BACKUP)) # files in compiler/ try: num_compiler_files = sum( 1 for file in utils.install_path('compiler').rglob('*')) except FileNotFoundError: num_compiler_files = 0 if self.steamID == utils.STEAM_IDS['APERTURE TAG']: # Coop paint gun instance num_compiler_files += 1 if num_compiler_files == 0: LOGGER.warning('No compiler files!') export_screen.skip_stage('COMP') else: export_screen.set_length('COMP', num_compiler_files) LOGGER.info('Should refresh: {}', should_refresh) if should_refresh: # Check to ensure the cache needs to be copied over.. should_refresh = self.cache_invalid() if should_refresh: LOGGER.info("Cache invalid - copying..") else: LOGGER.info("Skipped copying cache!") # Each object type # Editoritems # VBSP_config # Instance list # Editor models. # FGD file # Gameinfo export_screen.set_length('EXP', len(packages.OBJ_TYPES) + 6) # Do this before setting music and resources, # those can take time to compute. export_screen.show() try: if should_refresh: # Count the files. export_screen.set_length( 'RES', sum(1 for file in res_system.walk_folder_repeat()), ) else: export_screen.skip_stage('RES') export_screen.skip_stage('MUS') # Make the folders we need to copy files to, if desired. os.makedirs(self.abs_path('bin/bee2/'), exist_ok=True) # Start off with the style's data. vbsp_config = Property(None, []) vbsp_config += style.config.copy() all_items = style.items.copy() renderables = style.renderables.copy() export_screen.step('EXP') vpk_success = True # Export each object type. for obj_name, obj_data in packages.OBJ_TYPES.items(): if obj_name == 'Style': continue # Done above already LOGGER.info('Exporting "{}"', obj_name) selected = selected_objects.get(obj_name, None) try: obj_data.cls.export( packages.ExportData( game=self, selected=selected, all_items=all_items, renderables=renderables, vbsp_conf=vbsp_config, selected_style=style, )) except packages.NoVPKExport: # Raised by StyleVPK to indicate it failed to copy. vpk_success = False export_screen.step('EXP') vbsp_config.set_key(('Options', 'Game_ID'), self.steamID) vbsp_config.set_key( ('Options', 'dev_mode'), srctools.bool_as_int(optionWindow.DEV_MODE.get())) # If there are multiple of these blocks, merge them together. # They will end up in this order. vbsp_config.merge_children( 'Textures', 'Fizzlers', 'Options', 'StyleVars', 'DropperItems', 'Conditions', 'Quotes', 'PackTriggers', ) for name, file, ext in FILES_TO_BACKUP: item_path = self.abs_path(file + ext) backup_path = self.abs_path(file + '_original' + ext) if not os.path.isfile(item_path): # We can't backup at all. should_backup = False elif name == 'Editoritems': should_backup = not os.path.isfile(backup_path) else: # Always backup the non-_original file, it'd be newer. # But only if it's Valves - not our own. should_backup = should_backup_app(item_path) backup_is_good = should_backup_app(backup_path) LOGGER.info( '{}{}: normal={}, backup={}', file, ext, 'Valve' if should_backup else 'BEE2', 'Valve' if backup_is_good else 'BEE2', ) if not should_backup and not backup_is_good: # It's a BEE2 application, we have a problem. # Both the real and backup are bad, we need to get a # new one. try: os.remove(backup_path) except FileNotFoundError: pass try: os.remove(item_path) except FileNotFoundError: pass export_screen.reset() if messagebox.askokcancel( title=_('BEE2 - Export Failed!'), message=_( 'Compiler file {file} missing. ' 'Exit Steam applications, then press OK ' 'to verify your game cache. You can then ' 'export again.').format(file=file + ext, ), master=TK_ROOT, ): webbrowser.open('steam://validate/' + str(self.steamID)) return False, vpk_success if should_backup: LOGGER.info('Backing up original {}!', name) shutil.copy(item_path, backup_path) export_screen.step('BACK') # Backup puzzles, if desired backup.auto_backup(selected_game, export_screen) # Special-case: implement the UnlockDefault stlylevar here, # so all items are modified. if selected_objects['StyleVar']['UnlockDefault']: LOGGER.info('Unlocking Items!') for i, item in enumerate(all_items): # If the Unlock Default Items stylevar is enabled, we # want to force the corridors and obs room to be # deletable and copyable # Also add DESIRES_UP, so they place in the correct orientation if item.id in _UNLOCK_ITEMS: all_items[i] = copy.copy(item) item.deletable = item.copiable = True item.facing = editoritems.DesiredFacing.UP LOGGER.info('Editing Gameinfo...') self.edit_gameinfo(True) export_screen.step('EXP') if not GEN_OPTS.get_bool('General', 'preserve_bee2_resource_dir'): LOGGER.info('Adding ents to FGD.') self.edit_fgd(True) export_screen.step('EXP') # AtomicWriter writes to a temporary file, then renames in one step. # This ensures editoritems won't be half-written. LOGGER.info('Writing Editoritems script...') with srctools.AtomicWriter( self.abs_path('portal2_dlc2/scripts/editoritems.txt') ) as editor_file: editoritems.Item.export(editor_file, all_items, renderables) export_screen.step('EXP') LOGGER.info('Writing Editoritems database...') with open(self.abs_path('bin/bee2/editor.bin'), 'wb') as inst_file: pick = pickletools.optimize(pickle.dumps(all_items)) inst_file.write(pick) export_screen.step('EXP') LOGGER.info('Writing VBSP Config!') os.makedirs(self.abs_path('bin/bee2/'), exist_ok=True) with open(self.abs_path('bin/bee2/vbsp_config.cfg'), 'w', encoding='utf8') as vbsp_file: for line in vbsp_config.export(): vbsp_file.write(line) export_screen.step('EXP') if num_compiler_files > 0: LOGGER.info('Copying Custom Compiler!') compiler_src = utils.install_path('compiler') for comp_file in compiler_src.rglob('*'): # Ignore folders. if comp_file.is_dir(): continue dest = self.abs_path('bin' / comp_file.relative_to(compiler_src)) LOGGER.info('\t* {} -> {}', comp_file, dest) folder = Path(dest).parent if not folder.exists(): folder.mkdir(parents=True, exist_ok=True) try: if os.path.isfile(dest): # First try and give ourselves write-permission, # if it's set read-only. utils.unset_readonly(dest) shutil.copy(comp_file, dest) except PermissionError: # We might not have permissions, if the compiler is currently # running. export_screen.reset() messagebox.showerror( title=_('BEE2 - Export Failed!'), message=_('Copying compiler file {file} failed. ' 'Ensure {game} is not running.').format( file=comp_file, game=self.name, ), master=TK_ROOT, ) return False, vpk_success export_screen.step('COMP') if should_refresh: LOGGER.info('Copying Resources!') music_files = self.copy_mod_music() self.refresh_cache(music_files) LOGGER.info('Optimizing editor models...') self.clean_editor_models(all_items) export_screen.step('EXP') self.generate_fizzler_sides(vbsp_config) if self.steamID == utils.STEAM_IDS['APERTURE TAG']: os.makedirs(self.abs_path('sdk_content/maps/instances/bee2/'), exist_ok=True) with open( self.abs_path( 'sdk_content/maps/instances/bee2/tag_coop_gun.vmf' ), 'w') as f: TAG_COOP_INST_VMF.export(f) export_screen.reset() # Hide loading screen, we're done return True, vpk_success except loadScreen.Cancelled: return False, False