def load_settings(pit: Property):
    if not pit:
        SETTINGS.clear()
        # No pits are permitted..
        return

    SETTINGS.update({
        'use_skybox': srctools.conv_bool(pit['teleport', '0']),
        'tele_dest': pit['tele_target', '@goo_targ'],
        'tele_ref': pit['tele_ref', '@goo_ref'],
        'off_x': srctools.conv_int(pit['off_x', '0']),
        'off_y': srctools.conv_int(pit['off_y', '0']),
        'skybox': pit['sky_inst', ''],
        'skybox_ceil': pit['sky_inst_ceil', ''],
        'targ': pit['targ_inst', ''],
        'blend_light': pit['blend_light', '']
    })
    for inst_type in (
        'support',
        'side',
        'corner',
        'double',
        'triple',
        'pillar',
    ):
        vals = [
            prop.value
            for prop in
            pit.find_all(inst_type + '_inst')
        ]
        if len(vals) == 0:
            vals = [""]
        PIT_INST[inst_type] = vals
def res_camera_setup(res: Property):
    return {
        'cam_off': Vec.from_str(res['CamOff', '']),
        'yaw_off': Vec.from_str(res['YawOff', '']),
        'pitch_off': Vec.from_str(res['PitchOff', '']),
        'io_inst': resolve_inst(res['IO_inst'])[0],
        'yaw_inst': resolve_inst(res['yawInst', ''])[0],
        'pitch_inst': resolve_inst(res['pitchInst', ''])[0],
        'yaw_range': srctools.conv_int(res['YawRange', ''], 90),
        'pitch_range': srctools.conv_int(res['YawRange', ''], 90),
    }
Beispiel #3
0
def parse(
    vmf: VMF, pack: PackList
) -> Tuple[int, Dict[Tuple[str, str, int], VacObject], Dict[str, str], ]:
    """Parse out the cube objects from the map.

    The return value is the number of objects, a dict of objects, and the
    filenames of the script generated for each group.
    The dict is (group, model, skin) -> object.
    """
    cube_objects: Dict[Tuple[str, str, int], VacObject] = {}
    vac_objects: Dict[str, List[VacObject]] = defaultdict(list)

    for i, ent in enumerate(vmf.by_class['comp_vactube_object']):
        offset = Vec.from_str(ent['origin']) - Vec.from_str(ent['offset'])
        obj = VacObject(
            f'obj_{i:x}',
            ent['group'],
            ent['model'],
            ent['cube_model'],
            offset,
            srctools.conv_int(ent['weight']),
            srctools.conv_int(ent['tv_skin']),
            srctools.conv_int(ent['cube_skin']),
            srctools.conv_int(ent['skin']),
        )
        vac_objects[obj.group].append(obj)
        # Convert the ent into a precache ent, stripping the other keyvalues.
        ent.keys = {'model': ent['model']}
        make_precache_prop(ent)

        if obj.model_drop:
            cube_objects[obj.group,
                         obj.model_drop.replace('\\', '/'),
                         obj.skin_drop, ] = obj

    # Generate and pack the vactube object scripts.
    # Each group is the same, so it can be shared among them all.
    codes = {}
    for group in sorted(vac_objects):
        code = []
        for i, obj in enumerate(vac_objects[group]):
            if obj.model_drop:
                model_code = f'"{obj.model_drop}"'
            else:
                model_code = 'null'
            code.append(
                f'{obj.id} <- obj("{obj.model_vac}", {obj.skin_vac}, '
                f'{model_code}, {obj.weight}, "{obj.offset}", {obj.skin_tv});')
        codes[group] = pack.inject_vscript('\n'.join(code))

    return len(vac_objects), cube_objects, codes
Beispiel #4
0
def get_itemconf(
    name: Union[str, Tuple[str, str]],
    default: Optional[OptionType],
    timer_delay: int = None,
) -> Optional[OptionType]:
    """Get an itemconfig value.

    The name should be an 'ID:Section', or a tuple of the same.
    The type of the default sets what value it will be converted to.
    None returns the string, or None if not present.
    If set, timer_value is the value used for the timer.
    """
    if name == '':
        return default

    try:
        if isinstance(name, tuple):
            group_id, wid_id = name
        else:
            group_id, wid_id = name.split(':')
    except ValueError:
        LOGGER.warning('Invalid item config: {!r}!', name)
        return default

    wid_id = wid_id.casefold()

    if timer_delay is not None:
        if timer_delay < 3 or timer_delay > 30:
            wid_id += '_inf'
        else:
            wid_id += '_{}'.format(timer_delay)

    value = ITEM_CONFIG.get_val(group_id, wid_id, '')
    if not value:
        return default

    if isinstance(default, str) or default is None:
        return value
    elif isinstance(default, Vec):
        return Vec.from_str(value, default.x, default.y, default.z)
    elif isinstance(default, bool):
        return srctools.conv_bool(value, default)
    elif isinstance(default, float):
        return srctools.conv_int(value, default)
    elif isinstance(default, int):
        return srctools.conv_int(value, default)
    else:
        raise TypeError('Invalid default type "{}"!'.format(
            type(default).__name__))
Beispiel #5
0
def res_add_variant_setup(res: Property) -> object:
    if res.has_children():
        count = srctools.conv_int(res['Number', ''], None)
        if count:
            return conditions.weighted_random(
                count,
                res['weights', ''],
            )
        else:
            return None
    else:
        count = srctools.conv_int(res.value, None)
        if count:
            return list(range(count))
        else:
            return None
Beispiel #6
0
def res_random_setup(res: Property) -> object:
    weight = ''
    results = []
    chance = 100
    seed = 'b'
    for prop in res:
        if prop.name == 'chance':
            # Allow ending with '%' sign
            chance = srctools.conv_int(
                prop.value.rstrip('%'),
                chance,
            )
        elif prop.name == 'weights':
            weight = prop.value
        elif prop.name == 'seed':
            seed = 'b' + prop.value
        else:
            results.append(prop)

    if not results:
        return None  # Invalid!

    weight = conditions.weighted_random(len(results), weight)

    # We also need to execute result setups on all child properties!
    for prop in results[:]:
        if prop.name == 'group':
            for sub_prop in list(prop):
                Condition.setup_result(prop.value, sub_prop)
        else:
            Condition.setup_result(results, prop)

    return seed, chance, weight, results
def res_add_output_setup(res: Property):
    output = res['output']
    input_name = res['input']
    inst_in = res['inst_in', '']
    inst_out = res['inst_out', '']
    targ = res['target']
    only_once = srctools.conv_bool(res['only_once', None])
    times = 1 if only_once else srctools.conv_int(res['times', None], -1)
    delay = res['delay', '0.0']
    parm = res['parm', '']

    if output.startswith('<') and output.endswith('>'):
        out_id, out_type = output.strip('<>').split(':', 1)
        out_id = out_id.casefold()
        out_type = out_type.strip().casefold()
    else:
        out_id, out_type = output, 'const'

    return (
        out_type,
        out_id,
        targ,
        input_name,
        parm,
        delay,
        times,
        inst_in,
        inst_out,
    )
Beispiel #8
0
def test_conv_int():
    for string, result in ints:
        assert srctools.conv_int(string) == result, string

    # Check that float values fail
    marker = object()
    for string, result in floats:
        if isinstance(string, str):  # We don't want to check float-rounding
            assert srctools.conv_int(string, marker) is marker, repr(string)

    # Check non-integers return the default.
    for string in non_ints:
        assert srctools.conv_int(string) == 0
        for default in def_vals:
            # Check all default values pass through unchanged
            assert srctools.conv_int(string, default) is default, repr(string)
def res_add_variant_setup(res: Property):
    if res.has_children():
        count = srctools.conv_int(res['Number', ''], None)
        if count:
            return conditions.weighted_random(
                count,
                res['weights', ''],
            )
        else:
            return None
    else:
        count = srctools.conv_int(res.value, None)
        if count:
            return list(range(count))
        else:
            return None
def res_random_setup(res: Property):
    weight = ''
    results = []
    chance = 100
    seed = ''
    for prop in res:
        if prop.name == 'chance':
            # Allow ending with '%' sign
            chance = srctools.conv_int(
                prop.value.rstrip('%'),
                chance,
            )
        elif prop.name == 'weights':
            weight = prop.value
        elif prop.name == 'seed':
            seed = prop.value
        else:
            results.append(prop)

    if not results:
        return None  # Invalid!

    weight = conditions.weighted_random(len(results), weight)

    # We also need to execute result setups on all child properties!
    for prop in results[:]:
        if prop.name == 'group':
            for sub_prop in prop.value[:]:
                Condition.setup_result(prop.value, sub_prop)
        else:
            Condition.setup_result(results, prop)

    return seed, chance, weight, results
Beispiel #11
0
def test_conv_int():
    for string, result in ints:
        assert srctools.conv_int(string) == result, string

    # Check that float values fail
    marker = object()
    for string, result in floats:
        if isinstance(string, str):  # We don't want to check float-rounding
            assert srctools.conv_int(string, marker) is marker, repr(string)

    # Check non-integers return the default.
    for string in non_ints:
        assert srctools.conv_int(string) == 0
        for default in def_vals:
            # Check all default values pass through unchanged
            assert srctools.conv_int(string, default) is default, repr(string)
Beispiel #12
0
    def read_ent_data(self) -> VMF:
        """Parse in entity data.
        
        This returns a VMF object, with entities mirroring that in the BSP. 
        No brushes are read.
        """
        ent_data = self.get_lump(BSP_LUMPS.ENTITIES)
        vmf = VMF()
        cur_ent = None  # None when between brackets.
        seen_spawn = False  # The first entity is worldspawn.

        # This code performs the same thing as property_parser, but simpler
        # since there's no nesting, comments, or whitespace, except between
        # key and value. We also operate directly on the (ASCII) binary.
        for line in ent_data.splitlines():
            if line == b'{':
                if cur_ent is not None:
                    raise ValueError(
                        '2 levels of nesting after {} ents'.format(
                            len(vmf.entities)))
                if not seen_spawn:
                    cur_ent = vmf.spawn
                    seen_spawn = True
                else:
                    cur_ent = Entity(vmf)
            elif line == b'}':
                if cur_ent is None:
                    raise ValueError(
                        'Too many closing brackets after {} ents'.format(
                            len(vmf.entities)))
                if cur_ent is vmf.spawn:
                    if cur_ent['classname'] != 'worldspawn':
                        raise ValueError('No worldspawn entity!')
                else:
                    # The spawn ent is stored in the attribute, not in the ent
                    # list.
                    vmf.add_ent(cur_ent)
                cur_ent = None
            elif line == b'\x00':  # Null byte at end of lump.
                if cur_ent is not None:
                    raise ValueError("Last entity didn't end!")
                return vmf
            else:
                # Line is of the form <"key" "val">
                key, value = line.split(b'" "')
                decoded_key = key[1:].decode('ascii')
                decoded_val = value[:-1].decode('ascii')
                if 27 in value:
                    # All outputs use the comma_sep, so we can ID them.
                    cur_ent.add_out(
                        Output.parse(Property(decoded_key, decoded_val)))
                else:
                    # Normal keyvalue.
                    cur_ent[decoded_key] = decoded_val

        # This keyvalue needs to be stored in the VMF object too.
        # The one in the entity is ignored.
        vmf.map_ver = conv_int(vmf.spawn['mapversion'], vmf.map_ver)

        return vmf
Beispiel #13
0
def trigger_brush_input_filters(ctx: Context) -> None:
    """Copy spawnflags on top of the keyvalue.

    This way you get checkboxes you can easily control.
    """
    for ent in ctx.vmf.by_class['trigger_brush']:
        if conv_int(ent['spawnflags']):
            ent['InputFilter'] = ent['spawnflags']
Beispiel #14
0
def res_random(coll: collisions.Collisions,
               res: Property) -> conditions.ResultCallable:
    """Randomly choose one of the sub-results to execute.

    The `chance` value defines the percentage chance for any result to be
    chosen. `weights` defines the weighting for each result. Both are
    comma-separated, matching up with the results following. Wrap a set of
    results in a `group` property block to treat them as a single result to be
    executed in order.
    """
    weight_str = ''
    results = []
    chance = 100
    seed = ''
    for prop in res:
        if prop.name == 'chance':
            # Allow ending with '%' sign
            chance = srctools.conv_int(
                prop.value.rstrip('%'),
                chance,
            )
        elif prop.name == 'weights':
            weight_str = prop.value
        elif prop.name == 'seed':
            seed = 'b' + prop.value
        else:
            results.append(prop)

    if not results:
        # Does nothing
        return lambda e: None

    weights_list = rand.parse_weights(len(results), weight_str)

    # Note: We can't delete 'global' results, instead replace by 'dummy'
    # results that don't execute.
    # Otherwise the chances would be messed up.
    def apply_random(inst: Entity) -> None:
        """Pick a random result and run it."""
        rng = rand.seed(b'rand_res', inst, seed)
        if rng.randrange(100) > chance:
            return

        ind = rng.choice(weights_list)
        choice = results[ind]
        if choice.name == 'nop':
            pass
        elif choice.name == 'group':
            for sub_res in choice:
                if Condition.test_result(coll, inst, sub_res) is RES_EXHAUSTED:
                    sub_res.name = 'nop'
                    sub_res.value = ''
        else:
            if Condition.test_result(coll, inst, choice) is RES_EXHAUSTED:
                choice.name = 'nop'
                choice.value = ''

    return apply_random
Beispiel #15
0
def res_vactube_setup(res: Property):
    group = res['group', 'DEFAULT_GROUP']

    if group not in VAC_CONFIGS:
        # Store our values in the CONFIGS dictionary
        config, inst_configs = VAC_CONFIGS[group] = {}, {}
    else:
        # Grab the already-filled values, and add to them
        config, inst_configs = VAC_CONFIGS[group]

    for block in res.find_all("Instance"):
        # Configuration info for each instance set..
        conf = {
            # The three sizes of corner instance
            ('corner', 1):
            block['corner_small_inst', ''],
            ('corner', 2):
            block['corner_medium_inst', ''],
            ('corner', 3):
            block['corner_large_inst', ''],
            ('corner_temp', 1):
            block['temp_corner_small', ''],
            ('corner_temp', 2):
            block['temp_corner_medium', ''],
            ('corner_temp', 3):
            block['temp_corner_large', ''],

            # Straight instances connected to the next part
            'straight':
            block['straight_inst', ''],

            # Supports attach to the 4 sides of the straight part,
            # if there's a brush there.
            'support':
            block['support_inst', ''],
            'is_tsection':
            srctools.conv_bool(block['is_tsection', '0']),
            ('entry', 'wall'):
            block['entry_inst'],
            ('entry', 'floor'):
            block['entry_floor_inst'],
            ('entry', 'ceiling'):
            block['entry_ceil_inst'],
            'exit':
            block['exit_inst'],
        }

        for prop in block.find_all("File"):
            try:
                size, file = prop.value.split(":", 1)
            except ValueError:
                size = 1
                file = prop.value

            for inst in instanceLocs.resolve(file):
                inst_configs[inst] = conf, srctools.conv_int(size, 1)

    return group
Beispiel #16
0
 def update_disp(var_name: str, var_index: str, operation: str) -> None:
     """Whenever the string changes, update the displayed text."""
     seconds = conv_int(var.get(), -1)
     if min_value <= seconds <= max_value:
         disp_var.set('{}:{:02}'.format(seconds // 60, seconds % 60))
     else:
         LOGGER.warning('Bad timer value "{}" for "{}"!', var.get(), conf['id'])
         # Recurse, with a known safe value.
         var.set(values[0])
Beispiel #17
0
 def update_disp(var_name: str, var_index: str, operation: str) -> None:
     """Whenever the string changes, update the displayed text."""
     seconds = conv_int(var.get(), -1)
     if min_value <= seconds <= max_value:
         disp_var.set('{}:{:02}'.format(seconds // 60, seconds % 60))
     else:
         LOGGER.warning('Bad timer value "{}" for "{}"!', var.get(),
                        conf['id'])
         # Recurse, with a known safe value.
         var.set(values[0])
def move_rope(ent: Entity):
    """Implement move_rope and keyframe_rope resources."""
    old_shader_type = conv_int(ent['RopeShader'])
    if old_shader_type == 0:
        yield 'materials/cable/cable.vmt'
    elif old_shader_type == 1:
        yield 'materials/cable/rope.vmt'
    else:
        yield 'materials/cable/chain.vmt'
    yield 'materials/cable/rope_shadowdepth.vmt'
def move_rope(ent: Entity):
    """Implement move_rope and keyframe_rope resources."""
    old_shader_type = conv_int(ent['RopeShader'])
    if old_shader_type == 0:
        yield 'materials/cable/cable.vmt'
    elif old_shader_type == 1:
        yield 'materials/cable/rope.vmt'
    else:
        yield 'materials/cable/chain.vmt'
    yield 'materials/cable/rope_shadowdepth.vmt'
Beispiel #20
0
def move_rope(pack: PackList, ent: Entity) -> None:
    """Implement move_rope and keyframe_rope resources."""
    old_shader_type = conv_int(ent['RopeShader'])
    if old_shader_type == 0:
        pack.pack_file('materials/cable/cable.vmt', FileType.MATERIAL)
    elif old_shader_type == 1:
        pack.pack_file('materials/cable/rope.vmt', FileType.MATERIAL)
    else:
        pack.pack_file('materials/cable/chain.vmt', FileType.MATERIAL)
    pack.pack_file('materials/cable/rope_shadowdepth.vmt', FileType.MATERIAL)
Beispiel #21
0
def force_paintinmap(ctx: Context):
    """If paint entities are present, set paint in map to true."""
    # Already set, don't bother confirming.
    if conv_bool(ctx.vmf.spawn['paintinmap']):
        return

    if needs_paint(ctx.vmf):
        ctx.vmf.spawn['paintinmap'] = '1'
        # Ensure we have some blobs.
        if conv_int(ctx.vmf.spawn['maxblobcount']) == 0:
            ctx.vmf.spawn['maxblobcount'] = '250'
Beispiel #22
0
def force_paintinmap(ctx: Context):
    """If paint entities are present, set paint in map to true."""
    # Already set, don't bother confirming.
    if conv_bool(ctx.vmf.spawn['paintinmap']):
        return

    if needs_paint(ctx.vmf):
        ctx.vmf.spawn['paintinmap'] = '1'
        # Ensure we have some blobs.
        if conv_int(ctx.vmf.spawn['maxblobcount']) == 0:
            ctx.vmf.spawn['maxblobcount'] = '250'
Beispiel #23
0
def res_translate_inst(inst: Entity, res: Property):
    """Translate the instance locally by the given amount.

    The special values <piston>, <piston_bottom> and <piston_top> can be
    used to offset it based on the starting position, bottom or top position
    of a piston platform.
    """
    folded_val = res.value.casefold()
    if folded_val == '<piston>':
        folded_val = ('<piston_top>' if srctools.conv_bool(
            inst.fixup['$start_up']) else '<piston_bottom>')

    if folded_val == '<piston_top>':
        val = Vec(z=128 * srctools.conv_int(inst.fixup['$top_level', '1'], 1))
    elif folded_val == '<piston_bottom>':
        val = Vec(z=128 *
                  srctools.conv_int(inst.fixup['$bottom_level', '0'], 0))
    else:
        val = Vec.from_str(res.value)

    offset = val.rotate_by_str(inst['angles'])
    inst['origin'] = (offset + Vec.from_str(inst['origin'])).join(' ')
Beispiel #24
0
def flag_random(inst: Entity, res: Property) -> bool:
    """Randomly is either true or false."""
    if res.has_children():
        chance = res['chance', '100']
        seed = 'a' + res['seed', '']
    else:
        chance = res.value
        seed = 'a'

    # Allow ending with '%' sign
    chance = srctools.conv_int(chance.rstrip('%'), 100)

    set_random_seed(inst, seed)
    return random.randrange(100) < chance
Beispiel #25
0
def flag_random(inst: Entity, res: Property) -> bool:
    """Randomly is either true or false."""
    if res.has_children():
        chance = res['chance', '100']
        seed = 'a' + res['seed', '']
    else:
        chance = res.value
        seed = 'a'

    # Allow ending with '%' sign
    chance = srctools.conv_int(chance.rstrip('%'), 100)

    set_random_seed(inst, seed)
    return random.randrange(100) < chance
Beispiel #26
0
    def parse(cls, data: ParseData) -> 'QuotePack':
        """Parse a voice line definition."""
        selitem_data = SelitemData.parse(data.info)
        chars = {
            char.strip()
            for char in data.info['characters', ''].split(',') if char.strip()
        }

        # For Cave Johnson voicelines, this indicates what skin to use on the
        # portrait.
        port_skin = srctools.conv_int(data.info['caveSkin', None], None)

        try:
            monitor_data = data.info.find_key('monitor')
        except NoKeyError:
            mon_studio = mon_cam_loc = None
            mon_interrupt = mon_cam_pitch = mon_cam_yaw = 0
            mon_studio_actor = ''
            turret_hate = False
        else:
            mon_studio = monitor_data['studio']
            mon_studio_actor = monitor_data['studio_actor', '']
            mon_interrupt = monitor_data.float('interrupt_chance', 0)
            mon_cam_loc = monitor_data.vec('Cam_loc')
            mon_cam_pitch, mon_cam_yaw, _ = monitor_data.vec('Cam_angles')
            turret_hate = monitor_data.bool('TurretShoot')

        config = get_config(
            data.info,
            data.fsys,
            'voice',
            pak_id=data.pak_id,
            prop_name='file',
        )

        return cls(
            data.id,
            selitem_data,
            config,
            chars=chars,
            skin=port_skin,
            studio=mon_studio,
            studio_actor=mon_studio_actor,
            interrupt=mon_interrupt,
            cam_loc=mon_cam_loc,
            cam_pitch=mon_cam_pitch,
            cam_yaw=mon_cam_yaw,
            turret_hate=turret_hate,
        )
Beispiel #27
0
def res_translate_inst(inst: Entity, res: Property):
    """Translate the instance locally by the given amount.

    The special values <piston>, <piston_bottom> and <piston_top> can be
    used to offset it based on the starting position, bottom or top position
    of a piston platform.
    """
    folded_val = res.value.casefold()
    if folded_val == '<piston>':
        folded_val = (
            '<piston_top>' if
            srctools.conv_bool(inst.fixup['$start_up'])
            else '<piston_bottom>'
        )

    if folded_val == '<piston_top>':
        val = Vec(z=128 * srctools.conv_int(inst.fixup['$top_level', '1'], 1))
    elif folded_val == '<piston_bottom>':
        val = Vec(z=128 * srctools.conv_int(inst.fixup['$bottom_level', '0'], 0))
    else:
        val = Vec.from_str(res.value)

    offset = val.rotate_by_str(inst['angles'])
    inst['origin'] = (offset + Vec.from_str(inst['origin'])).join(' ')
Beispiel #28
0
    def parse(cls, gm_id, config: ConfigFile):
        steam_id = config.get_val(gm_id, 'SteamID', '<none>')
        if not steam_id.isdigit():
            raise ValueError('Game {} has invalid Steam ID: {}'.format(
                gm_id, steam_id))

        folder = config.get_val(gm_id, 'Dir', '')
        if not folder:
            raise ValueError('Game {} has no folder!'.format(gm_id))
        mod_times = {}

        for name, value in config.items(gm_id):
            if name.startswith('pack_mod_'):
                mod_times[name[9:].casefold()] = srctools.conv_int(value)

        return cls(gm_id, steam_id, folder, mod_times)
Beispiel #29
0
def flag_random(res: Property) -> Callable[[Entity], bool]:
    """Randomly is either true or false."""
    if res.has_children():
        chance = res['chance', '100']
        seed = res['seed', '']
    else:
        chance = res.value
        seed = ''

    # Allow ending with '%' sign
    chance = srctools.conv_int(chance.rstrip('%'), 100)

    def rand_func(inst: Entity) -> bool:
        """Apply the random chance."""
        return rand.seed(b'rand_flag', inst, seed).randrange(100) < chance

    return rand_func
Beispiel #30
0
def res_rand_inst_shift_setup(res: Property):
    min_x = srctools.conv_int(res['min_x', '0'])
    max_x = srctools.conv_int(res['max_x', '0'])
    min_y = srctools.conv_int(res['min_y', '0'])
    max_y = srctools.conv_int(res['max_y', '0'])
    min_z = srctools.conv_int(res['min_z', '0'])
    max_z = srctools.conv_int(res['max_z', '0'])

    return (
        min_x, max_x,
        min_y, max_y,
        min_z, max_z,
    )
def flag_random(inst: Entity, res: Property):
    """Randomly is either true or false."""
    if res.has_children():
        chance = res['chance', '100']
        seed = res['seed', '']
    else:
        chance = res.value
        seed = ''

    # Allow ending with '%' sign
    chance = srctools.conv_int(chance.rstrip('%'), 100)

    random.seed('random_chance_{}:{}_{}_{}'.format(
        seed,
        inst['targetname', ''],
        inst['origin'],
        inst['angles'],
    ))
    return random.randrange(100) < chance
Beispiel #32
0
    def parse(cls, gm_id: str, config: ConfigFile) -> 'Game':
        """Parse out the given game ID from the config file."""
        steam_id = config.get_val(gm_id, 'SteamID', '<none>')
        if not steam_id.isdigit():
            raise ValueError(f'Game {gm_id} has invalid Steam ID: {steam_id}')

        folder = config.get_val(gm_id, 'Dir', '')
        if not folder:
            raise ValueError(f'Game {gm_id} has no folder!')

        if not os.path.exists(folder):
            raise ValueError(
                f'Folder {folder} does not exist for game {gm_id}!')

        mod_times = {}

        for name, value in config.items(gm_id):
            if name.startswith('pack_mod_'):
                mod_times[name[9:].casefold()] = srctools.conv_int(value)

        return cls(gm_id, steam_id, folder, mod_times)
def func_breakable_surf(ent: Entity):
    """Additional materials required for func_breakable_surf"""
    yield 'models/brokenglass_piece.mdl'

    surf_type = conv_int(ent['surfacetype'])
    if surf_type == 1:  # Tile
        yield from (
            'models/brokentile/tilebroken_03a.mdl',
            'models/brokentile/tilebroken_03b.mdl',
            'models/brokentile/tilebroken_03c.mdl',
            'models/brokentile/tilebroken_03d.mdl',

            'models/brokentile/tilebroken_02a.mdl',
            'models/brokentile/tilebroken_02b.mdl',
            'models/brokentile/tilebroken_02c.mdl',
            'models/brokentile/tilebroken_02d.mdl',

            'models/brokentile/tilebroken_01a.mdl',
            'models/brokentile/tilebroken_01b.mdl',
            'models/brokentile/tilebroken_01c.mdl',
            'models/brokentile/tilebroken_01d.mdl',
        )
    elif surf_type == 0:  # Glass
        yield from (
            'models/brokenglass/glassbroken_solid.mdl',
            'models/brokenglass/glassbroken_01a.mdl',
            'models/brokenglass/glassbroken_01b.mdl',
            'models/brokenglass/glassbroken_01c.mdl',
            'models/brokenglass/glassbroken_01d.mdl',
            'models/brokenglass/glassbroken_02a.mdl',
            'models/brokenglass/glassbroken_02b.mdl',
            'models/brokenglass/glassbroken_02c.mdl',
            'models/brokenglass/glassbroken_02d.mdl',
            'models/brokenglass/glassbroken_03a.mdl',
            'models/brokenglass/glassbroken_03b.mdl',
            'models/brokenglass/glassbroken_03c.mdl',
            'models/brokenglass/glassbroken_03d.mdl',
        )
Beispiel #34
0
def load_connectionpoint(item: Item, ent: Entity) -> None:
    """Allow more conveniently defining connectionpoints."""
    origin = Vec.from_str(ent['origin'])
    angles = Angle.from_str(ent['angles'])
    if round(angles.pitch) != 0.0 or round(angles.roll) != 0.0:
        LOGGER.warning(
            "Connection Point at {} is not flat on the floor, PeTI doesn't allow this.",
            origin,
        )
        return
    try:
        side = ConnSide.from_yaw(round(angles.yaw))
    except ValueError:
        LOGGER.warning(
            "Connection Point at {} must point in a cardinal direction, not {}!",
            origin,
            angles,
        )
        return

    orient = Matrix.from_yaw(round(angles.yaw))

    center = (origin - (-56, 56, 0)) / 16
    center.z = 0
    center.y = -center.y
    try:
        offset = SKIN_TO_CONN_OFFSETS[ent['skin']] @ orient
    except KeyError:
        LOGGER.warning('Connection Point at {} has invalid skin "{}"!', origin)
        return
    ant_pos = Coord(round(center.x + offset.x), round(center.y - offset.y), 0)
    sign_pos = Coord(round(center.x - offset.x), round(center.y + offset.y), 0)

    group_str = ent['group_id']

    item.antline_points[side].append(
        AntlinePoint(ant_pos, sign_pos, conv_int(ent['priority']),
                     int(group_str) if group_str.strip() else None))
def func_breakable_surf(ent: Entity):
    """Additional materials required for func_breakable_surf"""
    yield 'models/brokenglass_piece.mdl'

    surf_type = conv_int(ent['surfacetype'])
    if surf_type == 1:  # Tile
        yield from (
            'models/brokentile/tilebroken_03a.mdl',
            'models/brokentile/tilebroken_03b.mdl',
            'models/brokentile/tilebroken_03c.mdl',
            'models/brokentile/tilebroken_03d.mdl',
            'models/brokentile/tilebroken_02a.mdl',
            'models/brokentile/tilebroken_02b.mdl',
            'models/brokentile/tilebroken_02c.mdl',
            'models/brokentile/tilebroken_02d.mdl',
            'models/brokentile/tilebroken_01a.mdl',
            'models/brokentile/tilebroken_01b.mdl',
            'models/brokentile/tilebroken_01c.mdl',
            'models/brokentile/tilebroken_01d.mdl',
        )
    elif surf_type == 0:  # Glass
        yield from (
            'models/brokenglass/glassbroken_solid.mdl',
            'models/brokenglass/glassbroken_01a.mdl',
            'models/brokenglass/glassbroken_01b.mdl',
            'models/brokenglass/glassbroken_01c.mdl',
            'models/brokenglass/glassbroken_01d.mdl',
            'models/brokenglass/glassbroken_02a.mdl',
            'models/brokenglass/glassbroken_02b.mdl',
            'models/brokenglass/glassbroken_02c.mdl',
            'models/brokenglass/glassbroken_02d.mdl',
            'models/brokenglass/glassbroken_03a.mdl',
            'models/brokenglass/glassbroken_03b.mdl',
            'models/brokenglass/glassbroken_03c.mdl',
            'models/brokenglass/glassbroken_03d.mdl',
        )
Beispiel #36
0
def needs_paint(vmf: VMF) -> bool:
    """Check if we have paint."""
    for ent_cls in [
        'prop_paint_bomb',
        'paint_sphere',
        'weapon_paintgun',  # Not in retail but someone might add it.
    ]:
        if vmf.by_class[ent_cls]:
            return True

    for ent in vmf.by_class['info_paint_sprayer']:
        # Special case, this makes sprayers only render visually, which
        # works even without the value set.
        if not conv_bool(ent['DrawOnly']):
            return True

    for ent_cls in [
        'prop_weighted_cube',
        'prop_physics_paintable',
    ]:
        for ent in vmf.by_class[ent_cls]:
            # If the cube is bouncy, enable paint.
            if conv_int(ent['paintpower', '4'], 4) != 4:
                return True
Beispiel #37
0
def needs_paint(vmf: VMF) -> bool:
    """Check if we have paint."""
    for ent_cls in [
        'prop_paint_bomb',
        'paint_sphere',
        'weapon_paintgun',  # Not in retail but someone might add it.
    ]:
        if vmf.by_class[ent_cls]:
            return True

    for ent in vmf.by_class['info_paint_sprayer']:
        # Special case, this makes sprayers only render visually, which
        # works even without the value set.
        if not conv_bool(ent['DrawOnly']):
            return True

    for ent_cls in [
        'prop_weighted_cube',
        'prop_physics_paintable',
    ]:
        for ent in vmf.by_class[ent_cls]:
            # If the cube is bouncy, enable paint.
            if conv_int(ent['paintpower', '4'], 4) != 4:
                return True
Beispiel #38
0
def res_add_overlay_inst(inst: Entity, res: Property):
    """Add another instance on top of this one.

    Values:
        File: The filename.
        Fixup Style: The Fixup style for the instance. '0' (default) is
            Prefix, '1' is Suffix, and '2' is None.
        Copy_Fixup: If true, all the $replace values from the original
            instance will be copied over.
        move_outputs: If true, outputs will be moved to this instance.
        offset: The offset (relative to the base) that the instance
            will be placed. Can be set to '<piston_top>' and
            '<piston_bottom>' to offset based on the configuration.
            '<piston_start>' will set it to the starting position, and
            '<piston_end>' will set it to the ending position.
            of piston platform handles.
        angles: If set, overrides the base instance angles. This does
            not affect the offset property.
        fixup/localfixup: Keyvalues in this block will be copied to the
            overlay entity.
            If the value starts with $, the variable will be copied over.
            If this is present, copy_fixup will be disabled.
    """

    angle = res["angles", inst["angles", "0 0 0"]]
    overlay_inst = vbsp.VMF.create_ent(
        classname="func_instance",
        targetname=inst["targetname", ""],
        file=resolve_inst(res["file", ""])[0],
        angles=angle,
        origin=inst["origin"],
        fixup_style=res["fixup_style", "0"],
    )
    # Don't run if the fixup block exists..
    if srctools.conv_bool(res["copy_fixup", "1"]):
        if "fixup" not in res and "localfixup" not in res:
            # Copy the fixup values across from the original instance
            for fixup, value in inst.fixup.items():
                overlay_inst.fixup[fixup] = value

    conditions.set_ent_keys(overlay_inst.fixup, inst, res, "fixup")

    if res.bool("move_outputs", False):
        overlay_inst.outputs = inst.outputs
        inst.outputs = []

    if "offset" in res:
        folded_off = res["offset"].casefold()
        # Offset the overlay by the given distance
        # Some special placeholder values:
        if folded_off == "<piston_start>":
            if srctools.conv_bool(inst.fixup["$start_up", ""]):
                folded_off = "<piston_top>"
            else:
                folded_off = "<piston_bottom>"
        elif folded_off == "<piston_end>":
            if srctools.conv_bool(inst.fixup["$start_up", ""]):
                folded_off = "<piston_bottom>"
            else:
                folded_off = "<piston_top>"

        if folded_off == "<piston_bottom>":
            offset = Vec(z=srctools.conv_int(inst.fixup["$bottom_level"]) * 128)
        elif folded_off == "<piston_top>":
            offset = Vec(z=srctools.conv_int(inst.fixup["$top_level"], 1) * 128)
        else:
            # Regular vector
            offset = Vec.from_str(conditions.resolve_value(inst, res["offset"]))

        offset.rotate_by_str(inst["angles", "0 0 0"])
        overlay_inst["origin"] = (offset + Vec.from_str(inst["origin"])).join(" ")
    return overlay_inst
Beispiel #39
0
    def read_ent_data(self) -> VMF:
        """Parse in entity data.
        
        This returns a VMF object, with entities mirroring that in the BSP. 
        No brushes are read.
        """
        ent_data = self.get_lump(BSP_LUMPS.ENTITIES)
        vmf = VMF()
        cur_ent = None  # None when between brackets.
        seen_spawn = False  # The first entity is worldspawn.

        # This code performs the same thing as property_parser, but simpler
        # since there's no nesting, comments, or whitespace, except between
        # key and value. We also operate directly on the (ASCII) binary.
        for line in ent_data.splitlines():
            if line == b'{':
                if cur_ent is not None:
                    raise ValueError(
                        '2 levels of nesting after {} ents'.format(
                            len(vmf.entities)))
                if not seen_spawn:
                    cur_ent = vmf.spawn
                    seen_spawn = True
                else:
                    cur_ent = Entity(vmf)
                continue
            elif line == b'}':
                if cur_ent is None:
                    raise ValueError(f'Too many closing brackets after'
                                     f' {len(vmf.entities)} ents!')
                if cur_ent is vmf.spawn:
                    if cur_ent['classname'] != 'worldspawn':
                        raise ValueError('No worldspawn entity!')
                else:
                    # The spawn ent is stored in the attribute, not in the ent
                    # list.
                    vmf.add_ent(cur_ent)
                cur_ent = None
                continue
            elif line == b'\x00':  # Null byte at end of lump.
                if cur_ent is not None:
                    raise ValueError("Last entity didn't end!")
                return vmf

            if cur_ent is None:
                raise ValueError("Keyvalue outside brackets!")

            # Line is of the form <"key" "val">
            key, value = line.split(b'" "')
            decoded_key = key[1:].decode('ascii')
            decoded_value = value[:-1].decode('ascii')

            # Now, we need to figure out if this is a keyvalue,
            # or connection.
            # If we're L4D+, this is easy - they use 0x1D as separator.
            # Before, it's a comma which is common in keyvalues.
            # Assume it's an output if it has exactly 4 commas, and the last two
            # successfully parse as numbers.
            if 27 in value:
                # All outputs use the comma_sep, so we can ID them.
                cur_ent.add_out(
                    Output.parse(Property(decoded_key, decoded_value)))
            elif value.count(b',') == 4:
                try:
                    cur_ent.add_out(
                        Output.parse(Property(decoded_key, decoded_value)))
                except ValueError:
                    cur_ent[decoded_key] = decoded_value
            else:
                # Normal keyvalue.
                cur_ent[decoded_key] = decoded_value

        # This keyvalue needs to be stored in the VMF object too.
        # The one in the entity is ignored.
        vmf.map_ver = conv_int(vmf.spawn['mapversion'], vmf.map_ver)

        return vmf
Beispiel #40
0
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,
            )
Beispiel #41
0
def vactube_transform(ctx: Context) -> None:
    """Implements the dynamic Vactube system."""
    all_nodes = list(nodes.parse(ctx.vmf))
    if not all_nodes:
        # No vactubes.
        return
    LOGGER.info('{} vactube nodes found.', len(all_nodes))
    LOGGER.debug('Nodes: {}', all_nodes)

    if ctx.studiomdl is None:
        raise ValueError('Vactubes present, but no studioMDL path provided! '
                         'Set the path to studiomdl.exe in srctools.vdf.')

    obj_count, vac_objects, objects_code = objects.parse(ctx.vmf, ctx.pack)
    groups = set(objects_code)

    if not obj_count:
        raise ValueError('Vactube nodes present, but no objects. '
                         'You need to add comp_vactube_objects to your map '
                         'to define the contents.')

    LOGGER.info('{} vactube objects found.', obj_count)

    # Now join all the nodes to each other.
    # Tubes only have 90 degree bends, so a system should mostly be formed
    # out of about 6 different normals. So group by that.
    inputs_by_norm: Dict[Tuple[float, float, float],
                         List[Tuple[Vec, nodes.Node]]] = defaultdict(list)

    for node in all_nodes:
        # Spawners have no inputs.
        if isinstance(node, nodes.Spawner):
            node.has_input = True
        else:
            inputs_by_norm[node.input_norm().as_tuple()].append(
                (node.vec_point(0.0), node))

    norm_inputs = [(Vec(norm), node_lst)
                   for norm, node_lst in inputs_by_norm.items()]

    sources: List[nodes.Spawner] = []

    LOGGER.info('Linking nodes...')
    for node in all_nodes:
        # Destroyers (or Droppers) have no inputs.
        if isinstance(node, nodes.Destroyer):
            continue
        for dest_type in node.out_types:
            node.outputs[dest_type] = find_closest(
                norm_inputs,
                node,
                node.vec_point(1.0, dest_type),
                node.output_norm(dest_type),
            )
        if isinstance(node, nodes.Spawner):
            sources.append(node)
            if node.group not in groups:
                group_warn = (f'Node {node} uses group "{node.group}", '
                              'which has no objects registered!')
                if '' in groups:
                    # Fall back to ignoring the group, using the default
                    # blank one which is present.
                    LOGGER.warning("{} Using blank group.", group_warn)
                    node.group = ""
                else:
                    raise ValueError(group_warn)

    # Run through them again, check to see if any miss inputs.
    for node in all_nodes:
        if not node.has_input:
            raise ValueError('No source found for junction '
                             f'{node.ent["targetname"]} at ({node.origin})!')

    LOGGER.info('Generating animations...')
    all_anims = animations.generate(sources)
    # Sort the animations by their start and end, so they ideally are consistent.
    all_anims.sort(key=lambda a: (a.start_node.origin, a.end_node.origin))

    anim_mdl_name = Path('maps', ctx.bsp_path.stem, 'vac_anim.mdl')

    # Now generate the animation model.
    # First wipe the model.
    full_loc = ctx.game.path / 'models' / anim_mdl_name
    for ext in MDL_EXTS:
        try:
            full_loc.with_suffix(ext).unlink()
        except FileNotFoundError:
            pass

    with TemporaryDirectory(prefix='vactubes_') as temp_dir:
        # Make the reference mesh.
        with open(temp_dir + '/ref.smd', 'wb') as f:
            Mesh.build_bbox('root', 'demo', Vec(-32, -32, -32),
                            Vec(32, 32, 32)).export(f)

        with open(temp_dir + '/prop.qc', 'w') as qc_file:
            qc_file.write(QC_TEMPLATE.format(path=anim_mdl_name))

            for i, anim in enumerate(all_anims):
                anim.name = anim_name = f'anim_{i:03x}'
                qc_file.write(
                    SEQ_TEMPLATE.format(name=anim_name, fps=animations.FPS))

                with open(temp_dir + f'/{anim_name}.smd', 'wb') as f:
                    anim.mesh.export(f)

        args = [
            str(ctx.studiomdl),
            '-nop4',
            '-i',  # Ignore warnings.
            '-game',
            str(ctx.game.path),
            temp_dir + '/prop.qc',
        ]
        LOGGER.info('Compiling vactube animations {}...', args)
        subprocess.run(args)

    # Ensure they're all packed.
    for ext in MDL_EXTS:
        try:
            f = full_loc.with_suffix(ext).open('rb')
        except FileNotFoundError:
            pass
        else:
            with f:
                ctx.pack.pack_file(Path('models',
                                        anim_mdl_name.with_suffix(ext)),
                                   data=f.read())

    LOGGER.info('Setting up vactube ents...')
    # Generate the shared template.
    ctx.vmf.create_ent(
        'prop_dynamic',
        targetname='_vactube_temp_mover',
        angles='0 270 0',
        origin='-16384 0 1024',
        model=str(Path('models', anim_mdl_name)),
        rendermode=10,
        solid=0,
        spawnflags=64 | 256,  # Use Hitboxes for Renderbox, collision disabled.
    )
    ctx.vmf.create_ent(
        'prop_dynamic_override',  # In case you use the physics model.
        targetname='_vactube_temp_visual',
        parentname='_vactube_temp_mover,move',
        origin='-16384 0 1024',
        model=nodes.CUBE_MODEL,
        solid=0,
        spawnflags=64 | 256,  # Use Hitboxes for Renderbox, collision disabled.
    )
    ctx.vmf.create_ent(
        'point_template',
        targetname='_vactube_template',
        template01='_vactube_temp_mover',
        template02='_vactube_temp_visual',
        origin='-16384 0 1024',
        spawnflags='2',  # Preserve names, remove originals.
    )

    # Group animations by their start point.
    anims_by_start: Dict[nodes.Spawner,
                         List[animations.Animation]] = defaultdict(list)

    for anim in all_anims:
        anims_by_start[anim.start_node].append(anim)

    # And create a dict to link droppers to the animation they want.
    dropper_to_anim: Dict[nodes.Dropper, animations.Animation] = {}

    for start_node, anims in anims_by_start.items():
        spawn_maker = start_node.ent
        spawn_maker['classname'] = 'env_entity_maker'
        spawn_maker['entitytemplate'] = '_vactube_template'
        spawn_maker['angles'] = '0 0 0'
        orig_name = spawn_maker['targetname']
        spawn_maker.make_unique('_vac_maker')
        spawn_name = spawn_maker['targetname']

        if start_node.is_auto:
            spawn_timer = ctx.vmf.create_ent(
                'logic_timer',
                targetname=spawn_name + '_timer',
                origin=start_node.origin,
                startdisabled='0',
                userandomtime='1',
                lowerrandombound=start_node.time_min,
                upperrandombound=start_node.time_max,
            ).make_unique()
            spawn_timer.add_out(
                Output('OnTimer', spawn_name, 'CallScriptFunction',
                       'make_cube'))
            ctx.add_io_remap(
                orig_name,
                Output('EnableTimer', spawn_timer, 'Enable'),
                Output('DisableTimer', spawn_timer, 'Disable'),
            )
        ctx.add_io_remap(
            orig_name,
            Output('ForceSpawn', spawn_name, 'CallScriptFunction',
                   'make_cube'),
        )

        # Now, generate the code so the VScript knows about the animations.
        code = [
            f'// Node: {start_node.ent["targetname"]}, {start_node.origin}'
        ]
        for anim in anims:
            target = anim.end_node
            anim_speed = anim.start_node.speed
            pass_code = ','.join([
                f'Output({time:.2f}, "{node.ent["targetname"]}", '
                f'{node.tv_code(anim_speed)})'
                for time, node in anim.pass_points
            ])
            cube_name = 'null'
            if isinstance(target, nodes.Dropper):
                cube_model = target.cube['model'].replace('\\', '/')
                cube_skin = conv_int(target.cube['skin'])
                try:
                    cube_name = vac_objects[start_node.group, cube_model,
                                            cube_skin].id
                except KeyError:
                    LOGGER.warning(
                        'Cube model "{}", skin {} is not a type of cube travelling '
                        'in this vactube!\n\n'
                        'Add a comp_vactube_object entity with this cube model'
                        # Mention groups if they're used, otherwise it's not important.
                        + (f' with the group "{start_node.group}".'
                           if start_node.group else '.'),
                        cube_model,
                        cube_skin,
                    )
                    continue  # Skip this animation so it's not broken.
                else:
                    dropper_to_anim[target] = anim
            code.append(f'{anim.name} <- anim("{anim.name}", {anim.duration}, '
                        f'{cube_name}, [{pass_code}]);')
        spawn_maker['vscripts'] = ' '.join([
            'srctools/vac_anim.nut',
            objects_code[start_node.group],
            ctx.pack.inject_vscript('\n'.join(code)),
        ])

    # Now, go through each dropper and generate their logic.
    for dropper, anim in dropper_to_anim.items():
        # Pick the appropriate output to fire once left the dropper.
        if dropper.cube['classname'] == 'prop_monster_box':
            cube_input = 'BecomeMonster'
        else:
            cube_input = 'EnablePortalFunnel'

        ctx.add_io_remap(
            dropper.ent['targetname'],
            # Used to dissolve the existing cube when respawning.
            Output('FireCubeUser1', dropper.cube['targetname'], 'FireUser1'),
            # Tell the spawn to redirect a cube to us.
            Output(
                'RequestSpawn',
                anim.start_node.ent['targetname'],
                'RunScriptCode',
                f'{anim.name}.req_spawn = true',
            ),
            Output('CubeReleased', '!activator', cube_input),
        )
Beispiel #42
0
def widget_minute_seconds(parent: tk.Frame, var: tk.StringVar,
                          conf: Property) -> tk.Misc:
    """A widget for specifying times - minutes and seconds.

    The value is saved as seconds.
    Max specifies the largest amount.
    """
    max_value = conf.int('max', 60)
    min_value = conf.int('min', 0)
    if min_value > max_value:
        raise ValueError('Bad min and max values!')

    values = timer_values(min_value, max_value)

    default_value = conv_int(var.get(), -1)
    if min_value <= default_value <= max_value:
        default_text = values[default_value - min_value]
    else:
        LOGGER.warning('Bad timer value "{}" for "{}"!', var.get(), conf['id'])
        default_text = '0:01'
        var.set('1')

    disp_var = tk.StringVar()

    def set_var():
        """Set the variable to the current value."""
        try:
            minutes, seconds = disp_var.get().split(':')
            var.set(int(minutes) * 60 + int(seconds))
        except (ValueError, TypeError):
            pass

    def validate(reason: str, operation_type: str, cur_value: str,
                 new_char: str, new_value: str):
        """Validate the values for the text."""
        if operation_type == '0' or reason == 'forced':
            # Deleting or done by the program, allow that always.
            return True

        if operation_type == '1':
            # Disallow non number and colons
            if new_char not in '0123456789:':
                return False
            # Only one colon.
            if ':' in cur_value and new_char == ':':
                return False

            # Don't allow more values if it has more than 2 numbers after
            # the colon - if there is one, and it's not in the last 3 characters.
            if ':' in new_value and ':' not in new_value[-3:]:
                return False

        if reason == 'focusout':
            # When leaving focus, apply range limits and set the var.
            try:
                minutes, seconds = new_value.split(':')
                seconds = int(minutes) * 60 + int(seconds)
            except (ValueError, TypeError):
                seconds = default_value
            if seconds < min_value:
                seconds = min_value
            if seconds > max_value:
                seconds = max_value
            disp_var.set('{}:{:02}'.format(seconds // 60, seconds % 60))
            var.set(seconds)
        return True

    validate_cmd = parent.register(validate)

    spinbox = tk.Spinbox(
        parent,
        exportselection=False,
        textvariable=disp_var,
        command=set_var,
        wrap=True,
        values=values,
        validate='all',
        # %args substitute the values for the args to validate_cmd.
        validatecommand=(validate_cmd, '%V', '%d', '%s', '%S', '%P'),
    )
    # We need to set this after, it gets reset to the first one.
    disp_var.set(default_text)
    return spinbox
Beispiel #43
0
def res_unst_scaffold(res: Property):
    """The condition to generate Unstationary Scaffolds.

    This is executed once to modify all instances.
    """
    # The instance types we're modifying
    if res.value not in SCAFFOLD_CONFIGS:
        # We've already executed this config group
        return RES_EXHAUSTED

    LOGGER.info("Running Scaffold Generator ({})...", res.value)
    TARG_INST, LINKS = SCAFFOLD_CONFIGS[res.value]
    del SCAFFOLD_CONFIGS[res.value]  # Don't let this run twice

    instances = {}
    # Find all the instances we're wanting to change, and map them to
    # targetnames
    for ent in vbsp.VMF.by_class["func_instance"]:
        file = ent["file"].casefold()
        targ = ent["targetname"]
        if file not in TARG_INST:
            continue
        config = TARG_INST[file]
        next_inst = set(out.target for out in ent.outputs)
        # Destroy these outputs, they're useless now!
        ent.outputs.clear()
        instances[targ] = {"ent": ent, "conf": config, "next": next_inst, "prev": None}

    # Now link each instance to its in and outputs
    for targ, inst in instances.items():
        scaff_targs = 0
        for ent_targ in inst["next"]:
            if ent_targ in instances:
                instances[ent_targ]["prev"] = targ
                inst["next"] = ent_targ
                scaff_targs += 1
            else:
                # If it's not a scaffold, it's probably an indicator_toggle.
                # We want to remove any them as well as the assoicated
                # antlines!
                for toggle in vbsp.VMF.by_target[ent_targ]:
                    conditions.remove_ant_toggle(toggle)
        if scaff_targs > 1:
            raise Exception("A scaffold item has multiple destinations!")
        elif scaff_targs == 0:
            inst["next"] = None  # End instance

    starting_inst = []
    # We need to find the start instances, so we can set everything up
    for inst in instances.values():
        if inst["prev"] is None and inst["next"] is None:
            # Static item!
            continue
        elif inst["prev"] is None:
            starting_inst.append(inst)

    # We need to make the link entities unique for each scaffold set,
    # otherwise the AllVar property won't work.
    group_counter = 0

    # Set all the instances and properties
    for start_inst in starting_inst:
        group_counter += 1
        ent = start_inst["ent"]
        for vals in LINKS.values():
            if vals["all"] is not None:
                ent.fixup[vals["all"]] = SCAFF_PATTERN.format(name=vals["name"], group=group_counter, index="*")

        should_reverse = srctools.conv_bool(ent.fixup["$start_reversed"])

        # Now set each instance in the chain, including first and last
        for index, inst in enumerate(scaff_scan(instances, start_inst)):
            ent, conf = inst["ent"], inst["conf"]
            orient = "floor" if Vec(0, 0, 1).rotate_by_str(ent["angles"]) == (0, 0, 1) else "wall"

            # Find the offset used for the logic ents
            offset = (conf["off_" + orient]).copy()
            if conf["is_piston"]:
                # Adjust based on the piston position
                offset.z += 128 * srctools.conv_int(
                    ent.fixup["$top_level" if ent.fixup["$start_up"] == "1" else "$bottom_level"]
                )
            offset.rotate_by_str(ent["angles"])
            offset += Vec.from_str(ent["origin"])

            if inst["prev"] is None:
                link_type = "start"
            elif inst["next"] is None:
                link_type = "end"
            else:
                link_type = "mid"

            if orient == "floor" and link_type != "mid" and conf["inst_end"] is not None:
                # Add an extra instance pointing in the direction
                # of the connected track. This would be the endcap
                # model.
                other_ent = instances[inst["next" if link_type == "start" else "prev"]]["ent"]

                other_pos = Vec.from_str(other_ent["origin"])
                our_pos = Vec.from_str(ent["origin"])
                link_dir = other_pos - our_pos
                link_ang = math.degrees(math.atan2(link_dir.y, link_dir.x))
                # Round to nearest 90 degrees
                # Add 45 so the switchover point is at the diagonals
                link_ang = (link_ang + 45) // 90 * 90
                vbsp.VMF.create_ent(
                    classname="func_instance",
                    targetname=ent["targetname"],
                    file=conf["inst_end"],
                    origin=offset.join(" "),
                    angles="0 {:.0f} 0".format(link_ang),
                )
                # Don't place the offset instance, this replaces that!
            elif conf["inst_offset"] is not None:
                # Add an additional rotated entity at the offset.
                # This is useful for the piston item.
                vbsp.VMF.create_ent(
                    classname="func_instance",
                    targetname=ent["targetname"],
                    file=conf["inst_offset"],
                    origin=offset.join(" "),
                    angles=ent["angles"],
                )

            logic_inst = vbsp.VMF.create_ent(
                classname="func_instance",
                targetname=ent["targetname"],
                file=conf.get("logic_" + link_type + ("_rev" if should_reverse else ""), ""),
                origin=offset.join(" "),
                angles=("0 0 0" if conf["rotate_logic"] else ent["angles"]),
            )
            for key, val in ent.fixup.items():
                # Copy over fixup values
                logic_inst.fixup[key] = val

            # Add the link-values
            for linkVar, link in LINKS.items():
                logic_inst.fixup[linkVar] = SCAFF_PATTERN.format(name=link["name"], group=group_counter, index=index)
                if inst["next"] is not None:
                    logic_inst.fixup[link["next"]] = SCAFF_PATTERN.format(
                        name=link["name"], group=group_counter, index=index + 1
                    )

            new_file = conf.get("inst_" + orient, "")
            if new_file != "":
                ent["file"] = new_file

    LOGGER.info("Finished Scaffold generation!")
    return RES_EXHAUSTED
Beispiel #44
0
def res_cutout_tile(res: Property):
    """Generate random quarter tiles, like in Destroyed or Retro maps.

    - "MarkerItem" is the instance to look for.
    - "TileSize" can be "2x2" or "4x4".
    - rotateMax is the amount of degrees to rotate squarebeam models.

    Materials:
    - "squarebeams" is the squarebeams variant to use.
    - "ceilingwalls" are the sides of the ceiling section.
    - "floorbase" is the texture under floor sections.
    - "tile_glue" is used on top of a thinner tile segment.
    - "clip" is the player_clip texture used over floor segments.
        (This allows customising the surfaceprop.)
    - "Floor4x4Black", "Ceil2x2White" and other combinations can be used to
       override the textures used.
    """
    item = resolve_inst(res['markeritem'])

    INST_LOCS = {}  # Map targetnames -> surface loc
    CEIL_IO = []  # Pairs of ceil inst corners to cut out.
    FLOOR_IO = []  # Pairs of floor inst corners to cut out.

    overlay_ids = {}  # When we replace brushes, we need to fix any overlays
    # on that surface.

    MATS.clear()
    floor_edges = []  # Values to pass to add_floor_sides() at the end

    sign_loc = set(FORCE_LOCATIONS)
    # If any signage is present in the map, we need to force tiles to
    # appear at that location!
    for over in conditions.VMF.by_class['info_overlay']:
        if (
                over['material'].casefold() in FORCE_TILE_MATS and
                # Only check floor/ceiling overlays
                over['basisnormal'] in ('0 0 1', '0 0 -1')
                ):
            loc = Vec.from_str(over['origin'])
            # Sometimes (light bridges etc) a sign will be halfway between
            # tiles, so in that case we need to force 2 tiles.
            loc_min = (loc - (15, 15, 0)) // 32 * 32  # type: Vec
            loc_max = (loc + (15, 15, 0)) // 32 * 32  # type: Vec
            loc_min += (16, 16, 0)
            loc_max += (16, 16, 0)
            FORCE_LOCATIONS.add(loc_min.as_tuple())
            FORCE_LOCATIONS.add(loc_max.as_tuple())

    SETTINGS = {
        'floor_chance': srctools.conv_int(
            res['floorChance', '100'], 100),
        'ceil_chance': srctools.conv_int(
            res['ceilingChance', '100'], 100),
        'floor_glue_chance': srctools.conv_int(
            res['floorGlueChance', '0']),
        'ceil_glue_chance': srctools.conv_int(
            res['ceilingGlueChance', '0']),

        'rotate_beams': int(srctools.conv_float(
            res['rotateMax', '0']) * BEAM_ROT_PRECISION),

        'beam_skin': res['squarebeamsSkin', '0'],

        'base_is_disp': srctools.conv_bool(res['dispBase', '0']),

        'quad_floor': res['FloorSize', '4x4'].casefold() == '2x2',
        'quad_ceil': res['CeilingSize', '4x4'].casefold() == '2x2',
    }

    random.seed(vbsp.MAP_RAND_SEED + '_CUTOUT_TILE_NOISE')
    noise = SimplexNoise(period=4 * 40)  # 4 tiles/block, 50 blocks max

    # We want to know the number of neighbouring tile cutouts before
    # placing tiles - blocks away from the sides generate fewer tiles.
    floor_neighbours = defaultdict(dict)  # all_floors[z][x,y] = count

    for mat_prop in res['Materials', []]:
        MATS[mat_prop.name].append(mat_prop.value)

    if SETTINGS['base_is_disp']:
        # We want the normal brushes to become nodraw.
        MATS['floorbase_disp'] = MATS['floorbase']
        MATS['floorbase'] = ['tools/toolsnodraw']

        # Since this uses random data for initialisation, the alpha and
        # regular will use slightly different patterns.
        alpha_noise = SimplexNoise(period=4 * 50)
    else:
        alpha_noise = None

    for key, default in TEX_DEFAULT:
        if key not in MATS:
            MATS[key] = [default]

    # Find our marker ents
    for inst in conditions.VMF.by_class['func_instance']: # type: VLib.Entity
        if inst['file'].casefold() not in item:
            continue
        targ = inst['targetname']
        orient = Vec(0, 0, 1).rotate_by_str(inst['angles', '0 0 0'])
        # Check the orientation of the marker to figure out what to generate
        if orient == (0, 0, 1):
            io_list = FLOOR_IO
        else:
            io_list = CEIL_IO

        # Reuse orient to calculate where the solid face will be.
        loc = (orient * -64) + Vec.from_str(inst['origin'])
        INST_LOCS[targ] = loc

        for out in inst.output_targets():
            io_list.append((targ, out))

        if not inst.outputs and inst.fixup['$connectioncount'] == '0':
            # If the item doesn't have any connections, 'connect'
            # it to itself so we'll generate a 128x128 tile segment.
            io_list.append((targ, targ))
        inst.remove()  # Remove the instance itself from the map.

    for start_floor, end_floor in FLOOR_IO:
        if end_floor not in INST_LOCS:
            # Not a marker - remove this and the antline.
            for toggle in conditions.VMF.by_target[end_floor]:
                conditions.remove_ant_toggle(toggle)
            continue

        box_min = Vec(INST_LOCS[start_floor])
        box_min.min(INST_LOCS[end_floor])

        box_max = Vec(INST_LOCS[start_floor])
        box_max.max(INST_LOCS[end_floor])

        if box_min.z != box_max.z:
            continue  # They're not in the same level!
        z = box_min.z

        if SETTINGS['rotate_beams']:
            # We have to generate 1 model per 64x64 block to do rotation...
            gen_rotated_squarebeams(
                box_min - (64, 64, 0),
                box_max + (64, 64, -8),
                skin=SETTINGS['beam_skin'],
                max_rot=SETTINGS['rotate_beams'],
            )
        else:
            # Make the squarebeams props, using big models if possible
            gen_squarebeams(
                box_min + (-64, -64, 0),
                box_max + (64, 64, -8),
                skin=SETTINGS['beam_skin']
            )

        # Add a player_clip brush across the whole area
        conditions.VMF.add_brush(conditions.VMF.make_prism(
            p1=box_min - (64, 64, FLOOR_DEPTH),
            p2=box_max + (64, 64, 0),
            mat=MATS['clip'][0],
        ).solid)

        # Add a noportal_volume covering the surface, in case there's
        # room for a portal.
        noportal_solid = conditions.VMF.make_prism(
            # Don't go all the way to the sides, so it doesn't affect wall
            # brushes.
            p1=box_min - (63, 63, 9),
            p2=box_max + (63, 63, 0),
            mat='tools/toolsinvisible',
        ).solid
        noportal_ent = conditions.VMF.create_ent(
            classname='func_noportal_volume',
            origin=box_min.join(' '),
        )
        noportal_ent.solids.append(noportal_solid)

        if SETTINGS['base_is_disp']:
            # Use displacements for the base instead.
            make_alpha_base(
                box_min + (-64, -64, 0),
                box_max + (64, 64, 0),
                noise=alpha_noise,
            )

        for x, y in utils.iter_grid(
                min_x=int(box_min.x),
                max_x=int(box_max.x) + 1,
                min_y=int(box_min.y),
                max_y=int(box_max.y) + 1,
                stride=128,
                ):
            # Build the set of all positions..
            floor_neighbours[z][x, y] = -1

        # Mark borders we need to fill in, and the angle (for func_instance)
        # The wall is the face pointing inwards towards the bottom brush,
        # and the ceil is the ceiling of the block above the bordering grid
        # points.
        for x in range(int(box_min.x), int(box_max.x) + 1, 128):
            # North
            floor_edges.append(BorderPoints(
                wall=Vec(x, box_max.y + 64, z - 64),
                ceil=Vec_tuple(x, box_max.y + 128, z),
                rot=270,
            ))
            # South
            floor_edges.append(BorderPoints(
                wall=Vec(x, box_min.y - 64, z - 64),
                ceil=Vec_tuple(x, box_min.y - 128, z),
                rot=90,
            ))

        for y in range(int(box_min.y), int(box_max.y) + 1, 128):
            # East
            floor_edges.append(BorderPoints(
                wall=Vec(box_max.x + 64, y, z - 64),
                ceil=Vec_tuple(box_max.x + 128, y, z),
                rot=180,
            ))

            # West
            floor_edges.append(BorderPoints(
                wall=Vec(box_min.x - 64, y, z - 64),
                ceil=Vec_tuple(box_min.x - 128, y, z),
                rot=0,
            ))

    # Now count boundries near tiles, then generate them.

    # Do it seperately for each z-level:
    for z, xy_dict in floor_neighbours.items():  # type: float, dict
        for x, y in xy_dict:  # type: float, float
            # We want to count where there aren't any tiles
            xy_dict[x, y] = (
                ((x - 128, y - 128) not in xy_dict) +
                ((x - 128, y + 128) not in xy_dict) +
                ((x + 128, y - 128) not in xy_dict) +
                ((x + 128, y + 128) not in xy_dict) +

                ((x - 128, y) not in xy_dict) +
                ((x + 128, y) not in xy_dict) +
                ((x, y - 128) not in xy_dict) +
                ((x, y + 128) not in xy_dict)
            )

        max_x = max_y = 0

        weights = {}
        # Now the counts are all correct, compute the weight to apply
        # for tiles.
        # Adding the neighbouring counts will make a 5x5 area needed to set
        # the center to 0.

        for (x, y), cur_count in xy_dict.items():
            max_x = max(x, max_x)
            max_y = max(y, max_y)

            # Orthrogonal is worth 0.2, diagonal is worth 0.1.
            # Not-present tiles would be 8 - the maximum
            tile_count = (
                0.8 * cur_count +
                0.1 * xy_dict.get((x - 128, y - 128), 8) +
                0.1 * xy_dict.get((x - 128, y + 128), 8) +
                0.1 * xy_dict.get((x + 128, y - 128), 8) +
                0.1 * xy_dict.get((x + 128, y + 128), 8) +

                0.2 * xy_dict.get((x - 128, y), 8) +
                0.2 * xy_dict.get((x, y - 128), 8) +
                0.2 * xy_dict.get((x, y + 128), 8) +
                0.2 * xy_dict.get((x + 128, y), 8)
            )
            # The number ranges from 0 (all tiles) to 12.8 (no tiles).
            # All tiles should still have a small chance to generate tiles.
            weights[x, y] = min((tile_count + 0.5) / 8, 1)

        # Share the detail entity among same-height tiles..
        detail_ent = conditions.VMF.create_ent(
            classname='func_detail',
        )

        for x, y in xy_dict:
            convert_floor(
                Vec(x, y, z),
                overlay_ids,
                MATS,
                SETTINGS,
                sign_loc,
                detail_ent,
                noise_weight=weights[x, y],
                noise_func=noise,
            )

    add_floor_sides(floor_edges)

    conditions.reallocate_overlays(overlay_ids)

    return conditions.RES_EXHAUSTED
Beispiel #45
0
def res_conveyor_belt(inst: Entity, res: Property):
    """Create a conveyor belt.

    Options:
        SegmentInst: Generated at each square. ('track' is the name of the path.)
        TrackTeleport: Set the track points so they teleport trains to the start.
        Speed: The fixup or number for the train speed.
        MotionTrig: If set, a trigger_multiple will be spawned that EnableMotions
          weighted cubes. The value is the name of the relevant filter.
        EndOutput: Adds an output to the last track. The value is the same as
          outputs in VMFs.
        RotateSegments: If true (default), force segments to face in the
          direction of movement
        RailTemplate: A template for the railings. This is made into a non-solid
          func_brush, combining all sections.
    """
    move_dist = srctools.conv_int(inst.fixup['$travel_distance'])

    if move_dist <= 2:
        # There isn't room for a catwalk, so don't bother.
        inst.remove()
        return

    move_dir = Vec(1, 0, 0).rotate_by_str(inst.fixup['$travel_direction'])
    move_dir.rotate_by_str(inst['angles'])
    start_offset = srctools.conv_float(inst.fixup['$starting_position'], 0)
    teleport_to_start = res.bool('TrackTeleport', True)
    segment_inst_file = res['SegmentInst', '']
    rail_template = res['RailTemplate', None]

    vmf = inst.map

    if segment_inst_file:
        segment_inst_file = conditions.resolve_inst(segment_inst_file)[0]

    track_speed = res['speed', None]

    start_pos = Vec.from_str(inst['origin'])
    end_pos = start_pos + move_dist * move_dir

    if start_offset > 0:
        # If an oscillating platform, move to the closest side..
        offset = start_offset * move_dir
        # The instance is placed this far along, so move back to the end.
        start_pos -= offset
        end_pos -= offset
        if start_offset > 0.5:
            # Swap the direction of movement..
            start_pos, end_pos = end_pos, start_pos
        inst['origin'] = start_pos

    # Find the angle which generates an instance pointing in the direction
    # of movement, with the same normal.
    norm = Vec(z=1).rotate_by_str(inst['angles'])
    for roll in range(0, 360, 90):
        angles = move_dir.to_angle(roll)
        if Vec(z=1).rotate(*angles) == norm:
            break
    else:
        raise ValueError(
            "Can't find angles to give a"
            ' z={} and x={}!'.format(norm, move_dir)
        )

    if res.bool('rotateSegments', True):
        inst['angles'] = angles
    else:
        angles = Vec.from_str(inst['angles'])

    # Add the EnableMotion trigger_multiple seen in platform items.
    # This wakes up cubes when it starts moving.
    motion_filter = res['motionTrig', None]

    # Disable on walls, or if the conveyor can't be turned on.
    if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0':
        motion_filter = None

    track_name = conditions.local_name(inst, 'segment_{}')
    rail_temp_solids = []
    last_track = None
    # Place beams at the top, so they don't appear inside wall sections.
    beam_start = start_pos + 48 * norm  # type: Vec
    beam_end = end_pos + 48 * norm  # type: Vec
    for index, pos in enumerate(beam_start.iter_line(beam_end, stride=128), start=1):
        track = vmf.create_ent(
            classname='path_track',
            targetname=track_name.format(index) + '-track',
            origin=pos,
            spawnflags=0,
            orientationtype=0,  # Don't rotate
        )
        if track_speed is not None:
            track['speed'] = track_speed
        if last_track:
            last_track['target'] = track['targetname']

        if index == 1 and teleport_to_start:
            track['spawnflags'] = 16  # Teleport here..

        last_track = track

        # Don't place at the last point - it doesn't teleport correctly,
        # and would be one too many.
        if segment_inst_file and pos != end_pos:
            seg_inst = vmf.create_ent(
                classname='func_instance',
                targetname=track_name.format(index),
                file=segment_inst_file,
                origin=pos,
                angles=angles,
            )
            seg_inst.fixup.update(inst.fixup)

        if rail_template:
            temp = conditions.import_template(
                rail_template,
                pos,
                angles,
                force_type=conditions.TEMP_TYPES.world,
                add_to_map=False,
            )
            rail_temp_solids.extend(temp.world)

    if rail_temp_solids:
        vmf.create_ent(
            classname='func_brush',
            origin=beam_start,
            spawnflags=1,  # Ignore +USE
            solidity=1,  # Not solid
            vrad_brush_cast_shadows=1,
            drawinfastreflection=1,
        ).solids = rail_temp_solids

    if teleport_to_start:
        # Link back to the first track..
        last_track['target'] = track_name.format(1) + '-track'

    # Generate an env_beam pointing from the start to the end of the track.
    beam_keys = res.find_key('BeamKeys', [])
    if beam_keys.value:
        beam = vmf.create_ent(classname='env_beam')

        # 3 offsets - x = distance from walls, y = side, z = height
        beam_off = beam_keys.vec('origin', 0, 63, 56)

        for prop in beam_keys:
            beam[prop.real_name] = prop.value

        # Localise the targetname so it can be triggered..
        beam['LightningStart'] = beam['targetname'] = conditions.local_name(
            inst,
            beam['targetname', 'beam']
        )
        del beam['LightningEnd']
        beam['origin'] = start_pos + Vec(
            -beam_off.x, beam_off.y, beam_off.z,
        ).rotate(*angles)
        beam['TargetPoint'] = end_pos + Vec(
            +beam_off.x, beam_off.y, beam_off.z,
        ).rotate(*angles)

    # Allow adding outputs to the last path_track.
    for prop in res.find_all('EndOutput'):
        output = Output.parse(prop)
        output.output = 'OnPass'
        output.inst_out = None
        output.comma_sep = False
        output.target = conditions.local_name(inst, output.target)
        last_track.add_out(output)

    if motion_filter is not None:
        motion_trig = vmf.create_ent(
            classname='trigger_multiple',
            targetname=conditions.local_name(inst, 'enable_motion_trig'),
            origin=start_pos,
            filtername=motion_filter,
            startDisabled=1,
            wait=0.1,
        )
        motion_trig.add_out(Output('OnStartTouch', '!activator', 'ExitDisabledState'))
        # Match the size of the original...
        motion_trig.solids.append(vmf.make_prism(
            start_pos + Vec(72, -56, 58).rotate(*angles),
            end_pos + Vec(-72, 56, 144).rotate(*angles),
            mat='tools/toolstrigger',
        ).solid)

    if res.bool('NoPortalFloor'):
        # Block portals on the floor..
        floor_noportal = vmf.create_ent(
            classname='func_noportal_volume',
            origin=beam_start,
        )
        floor_noportal.solids.append(vmf.make_prism(
            start_pos + Vec(-60, -60, -66).rotate(*angles),
            end_pos + Vec(60, 60, -60).rotate(*angles),
            mat='tools/toolsinvisible',
        ).solid)

    # A brush covering under the platform.
    base_trig = vmf.make_prism(
        start_pos + Vec(-64, -64, 48).rotate(*angles),
        end_pos + Vec(64, 64, 56).rotate(*angles),
        mat='tools/toolsinvisible',
    ).solid

    vmf.add_brush(base_trig)

    # Make a paint_cleanser under the belt..
    if res.bool('PaintFizzler'):
        pfizz = vmf.create_ent(
            classname='trigger_paint_cleanser',
            origin=start_pos,
        )
        pfizz.solids.append(base_trig.copy())
        for face in pfizz.sides():
            face.mat = 'tools/toolstrigger'