コード例 #1
0
def generate_coop_responses(ctx: Context) -> None:
    """If the entities are present, add the coop response script."""
    responses: Dict[str, List[str]] = {}
    for response in ctx.vmf.by_class['bee2_coop_response']:
        responses[response['type']] = [
            value for key, value in response.keys.items()
            if key.startswith('choreo')
        ]
        response.remove()

    if not responses:
        return

    script = ["BEE2_RESPONSES <- {"]
    for response_type, lines in sorted(responses.items()):
        script.append(f'\t{response_type} = [')
        for line in lines:
            script.append(f'\t\tCreateSceneEntity("{line}"),')
        script.append('\t],')
    script.append('};')

    # We want to write this onto the '@glados' entity.
    ent: Optional[Entity] = None
    for ent in ctx.vmf.by_target['@glados']:
        ctx.add_code(ent, '\n'.join(script))
        # Also include the actual script.
        split_script = ent['vscripts'].split()
        split_script.append('bee2/coop_responses.nut')
        ent['vscripts'] = ' '.join(split_script)

    if ent is None:
        LOGGER.warning('Response scripts present, but @glados is not!')
コード例 #2
0
ファイル: globals.py プロジェクト: kb173/srctools
def vscript_init_code(ctx: Context):
    """Add vscript_init_code keyvalues.

    The specified code is appended as a script file to the end of the scripts.
    vscript_init_code2, 3 etc will also be added in order if they exist.
    """
    for ent in ctx.vmf.entities:
        code = ent.keys.pop('vscript_init_code', '')

        if not code:
            continue

        for i in itertools.count(2):
            extra = ent.keys.pop('vscript_init_code' + str(i), '')
            if not extra:
                break
            code += '\n' + extra

        ctx.add_code(ent, code)
コード例 #3
0
ファイル: comp_relay.py プロジェクト: kb173/srctools
def comp_relay(ctx: Context):
    """Implements comp_relay, allowing zero-overhead relay ents for managing outputs.

    These are collapsed into their callers.
    """
    # Output -> input that we convert.
    out_names = {
        'ontrigger': 'trigger',
        'onturnedon': 'turnon',
        'onturnedoff': 'turnoff',
    }
    # Add user outputs as well.
    for i in '12345678':
        out_names['onuser' + i] = 'fireuser' + i

    for relay in ctx.vmf.by_class['comp_relay']:
        # First, see if any entities exist with the same name that aren't
        # comp_relays. In that case, we need to keep the inputs.
        relay_name = relay['targetname']
        should_remove = not any(ent['classname'].casefold() != 'comp_relay'
                                for ent in ctx.vmf.by_target[relay_name])
        # If ctrl_type is 0, ctrl_value needs to be 1 to be enabled.
        # If ctrl_type is 1, ctrl_value needs to be 0 to be enabled.
        enabled = conv_bool(relay['ctrl_type']) != conv_bool(
            relay['ctrl_value'])
        for out in relay.outputs:
            try:
                inp_name = out_names[out.output.casefold()]
            except KeyError:
                LOGGER.warning(
                    'Unknown output "{}" on comp_relay "{}"!\n'
                    'This will be discarded.',
                    out.output,
                    relay_name,
                )
                continue
            if enabled:
                out.output = inp_name
                ctx.add_io_remap(relay_name, out, remove=should_remove)
            elif should_remove:  # Still add a remap, to remove the outputs.
                ctx.add_io_remap_removal(relay_name, inp_name)
        relay.remove()
コード例 #4
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),
        )
コード例 #5
0
def comp_scriptvar(ctx: Context):
    """An entity to allow setting VScript variables to information from the map."""
    # {ent: {variable: data}}
    set_vars = defaultdict(
        lambda: defaultdict(VarData))  # type: Dict[Entity, Dict[str, VarData]]
    # If the index is None, there's no index.
    # If an int, that specific one.
    # If ..., blank index and it's inserted anywhere that fits.

    for comp_ent in ctx.vmf.by_class['comp_scriptvar_setter']:
        comp_ent.remove()
        var_name = orig_var_name = comp_ent['variable']
        index = None  # type: Union[int, Type[Ellipsis], None]

        parsed_match = re.fullmatch(r'\s*([^[]+)\[([0-9]*)\]\s*', var_name)
        if parsed_match:
            var_name, index_str = parsed_match.groups()
            if index_str:
                try:
                    index = int(index_str)
                    if index < 0:
                        raise ValueError  # No negatives.
                except (TypeError, ValueError):
                    LOGGER.warning(
                        'Invalid variable index in '
                        'comp_scriptvar at {} targetting "{}"!',
                        comp_ent['origin'], comp_ent['target'])
                    continue
            else:
                index = ...
        elif '[' in var_name or ']' in var_name:
            LOGGER.warning(
                'Unparsable variable[index] in '
                'comp_scriptvar at {} targetting "{}"!', comp_ent['origin'],
                comp_ent['target'])
            continue

        ref_name = comp_ent['ref']
        ref_ent = comp_ent
        if ref_name:
            for ref_ent in ctx.vmf.search(ref_name):
                break
            else:
                LOGGER.warning(
                    'Can\'t find ref entity named "{}" '
                    'for comp_scriptvar at <{}>!',
                    ref_name,
                    comp_ent['origin'],
                )

        try:
            mode_func = MODES[comp_ent['mode']]
        except KeyError:
            LOGGER.warning(
                'Invalid mode "{}" in '
                'comp_scriptvar at {} targeting "{}"!',
                comp_ent['mode'],
                comp_ent['origin'],
                comp_ent['target'],
            )
            continue
        else:
            code = mode_func(comp_ent, ref_ent)

        ent = None
        for ent in ctx.vmf.search(comp_ent['target']):
            var_data = set_vars[ent][var_name]
            # Now we've got to match the assignment this is doing
            # with other scriptvars.

            # First, take care of scalar assignment.
            if index is None:
                if var_data.is_array:
                    LOGGER.warning(
                        "comp_scriptvar at {} can't set a non-array value "
                        'on top of the array {} on the entity "{}"!',
                        comp_ent['origin'],
                        var_name,
                        comp_ent['target'],
                    )
                elif var_data.scalar is not None:
                    LOGGER.warning(
                        'comp_scriptvar at {} overwrote '
                        'the variable {} on "{}"!',
                        comp_ent['origin'],
                        var_name,
                        comp_ent['target'],
                    )
                else:
                    var_data.scalar = code
                continue
            # Else, we're setting an array value.
            if var_data.scalar is not None:
                LOGGER.warning(
                    "comp_scriptvar at {} can't set an array value "
                    'on top of the non-array {} on the entity "{}"!',
                    comp_ent['origin'],
                    var_name,
                    comp_ent['target'],
                )
                continue
            if index is Ellipsis:
                var_data.extra_pos.add(code)
            else:
                # Allow duplicates that write the exact same thing,
                # as a special case.
                if var_data.specified_pos.get(index, code) != code:
                    LOGGER.warning(
                        "comp_scriptvar at {} can't "
                        'overwrite {}[{}] on the entity "{}"!',
                        comp_ent['origin'],
                        var_name,
                        index,
                        comp_ent['target'],
                    )
                else:
                    var_data.specified_pos[index] = code

        if ent is None:
            # No targets?
            LOGGER.warning(
                'No entities found with name "{}", for '
                'comp_scriptvar at {}!',
                comp_ent['target'],
                comp_ent['origin'],
            )

    for ent, var_dict in set_vars.items():
        full_code = []
        for var_name, var_data in var_dict.items():
            full_code.append('{} <- {};'.format(var_name,
                                                var_data.make_code()))
        if full_code:
            ctx.add_code(ent, '\n'.join(full_code))