def test_parse(py_c_token):
    """Test parsing strings."""
    result = Property.parse(
        # iter() ensures sequence methods aren't used anywhere.
        iter(parse_test.splitlines()),
        # Check active and inactive flags are correctly treated.
        flags={
            'test_enabled': True,
            'test_disabled': False,
        }
    )
    assert_tree(parse_result, result)

    # Test the whole string can be passed too.
    result = Property.parse(
        parse_test,
        flags={
            'test_enabled': True,
            'test_disabled': False,
        },
    )
    assert_tree(parse_result, result)

    # Check export roundtrips.
    assert_tree(parse_result, Property.parse(parse_result.export()))
예제 #2
0
    def write_manifest(self, map_name: str=None) -> None:
        """Produce and pack a manifest file for this map.

        If map_name is provided, the script in the custom content position
        to be automatically loaded for that name. Otherwise, it will be packed
        such that it can override the master manifest with
        sv_soundemitter_flush.
        """
        manifest = Property('game_sounds_manifest', [
            Property('precache_file', snd)
            for snd, is_enabled in self.soundscript_files.items()
            if is_enabled is SoundScriptMode.INCLUDE
        ])

        buf = bytearray()
        for line in manifest.export():
            buf.extend(line.encode('utf8'))

        self.pack_file(
            'map/{}_level_sounds.txt'.format(map_name)
            if map_name else
            'scripts/game_sounds_manifest.txt',
            FileType.SOUNDSCRIPT,
            bytes(buf),
        )
예제 #3
0
    def write_manifest(self, map_name: str=None) -> None:
        """Produce and pack a manifest file for this map.

        If map_name is provided, the script in the custom content position
        to be automatically loaded for that name. Otherwise, it will be packed
        such that it can override the master manifest with
        sv_soundemitter_flush.
        """
        manifest = Property('game_sounds_manifest', [
            Property('precache_file', snd)
            for snd, is_enabled in self.soundscript_files.items()
            if is_enabled is SoundScriptMode.INCLUDE
        ])

        buf = bytearray()
        for line in manifest.export():
            buf.extend(line.encode('utf8'))

        self.pack_file(
            'map/{}_level_sounds.txt'.format(map_name)
            if map_name else
            'scripts/game_sounds_manifest.txt',
            FileType.SOUNDSCRIPT,
            bytes(buf),
        )
def test_bool():
    """Check bool(Property)."""
    assert bool(Property('Name', '')) is False
    assert bool(Property('Name', 'value')) is True
    assert bool(Property('Name', [])) is False
    assert bool(Property('Name', [
        Property('Key', 'Value')
    ])) is True
def test_constructor():
    """Test the constructor for Property objects."""
    Property(None, [])
    Property('Test', 'value with spaces and ""')
    block = Property('Test_block', [
        Property('Test', 'value\0'),
        Property('Test', [
            Property('leaf', 'data'),
        ]),
        Property('Test2', 'other'),
        Property('Block', []),
    ])
    assert block.real_name == 'Test_block'
    children = list(block)
    assert children[0].real_name == 'Test'
    assert children[1].real_name == 'Test'
    assert children[2].real_name == 'Test2'
    assert children[3].real_name == 'Block'

    assert children[0].value == 'value\0'
    assert children[2].value, 'other'
    assert list(children[3]) == []

    sub_children = list(children[1])
    assert sub_children[0].real_name == 'leaf'
    assert sub_children[0].value == 'data'
    assert len(sub_children) == 1
예제 #6
0
    def load_soundscript(
        self,
        file: File,
        *,
        always_include: bool=False,
    ) -> Iterable[str]:
        """Read in a soundscript and record which files use it.

        If always_include is True, it will be included in the manifests even
        if it isn't used.

        The sounds registered by this soundscript are returned.
        """
        try:
            with file.sys, file.open_str() as f:
                props = Property.parse(f, file.path, allow_escapes=False)
        except FileNotFoundError:
            # It doesn't exist, complain and pretend it's empty.
            LOGGER.warning('Soundscript "{}" does not exist!', file.path)
            return ()
        except KeyValError:
            LOGGER.warning('Soundscript "{}" could not be parsed:', exc_info=True)
            return ()

        return self._parse_soundscript(props, file.path, always_include)
 def check(prop: Property, name, value):
     """Check the property was edited, and has the given value."""
     nonlocal test_prop
     assert prop is test_prop
     assert prop.real_name == name
     assert prop.value == value
     test_prop = Property('Name', 'Value')
예제 #8
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
def test_names():
    """Test the behaviour of Property.name."""
    prop = Property('Test1', 'value')
    
    # Property.name casefolds the argument.
    assert prop.name == 'test1'
    assert prop.real_name == 'Test1'
    
    # Editing name modifies both values
    prop.name = 'SECOND_test'
    assert prop.name == 'second_test'
    assert prop.real_name == 'SECOND_test'
    
    # It can also be set to None.
    prop.name = None
    assert prop.name is prop.real_name is None
예제 #10
0
 def pack_breakable_chunk(self, chunkname: str) -> None:
     """Pack the generic gib model for the given chunk name."""
     if self._break_chunks is None:
         # Need to load the file.
         self.pack_file('scripts/propdata.txt')
         try:
             propdata = self.fsys['scripts/propdata.txt']
         except FileNotFoundError:
             LOGGER.warning('No scripts/propdata.txt for breakable chunks!')
             return
         with propdata.open_str() as f:
             props = Property.parse(f,
                                    'scripts/propdata.txt',
                                    allow_escapes=False)
         self._break_chunks = {}
         for chunk_prop in props.find_children('BreakableModels'):
             self._break_chunks[chunk_prop.name] = [
                 prop.real_name for prop in chunk_prop
             ]
     try:
         mdl_list = self._break_chunks[chunkname.casefold()]
     except KeyError:
         LOGGER.warning('Unknown gib chunks type "{}"!', chunkname)
         return
     for mdl in mdl_list:
         self.pack_file(mdl, FileType.MODEL)
예제 #11
0
    def read_prop(self, path: str, encoding='utf8') -> Property:
        """Read a Property file from the filesystem.

        This handles opening and closing files.
        """
        with self, self.open_str(path, encoding) as file:
            return Property.parse(
                file,
                self.path + ':' + path,
            )
 def t(text):
     """Test a string to ensure it fails parsing."""
     try:
         result = Property.parse(text)
     except KeyValError:
         pass
     else:
         pytest.fail("Successfully parsed bad text ({!r}) to {!r}".format(
             text,
             result,
         ))
def test_build():
    """Test the .build() constructor."""
    prop = Property(None, [])

    with prop.build() as b:
        with b.Root1:
            b.Key("Value")
            b.Extra("Spaces")
            with b.Block:
                with b.Empty:
                    pass
            with b.Block:
                with b.bare:
                    b.block('he\tre')
        with b.Root2:
            b['Name with " in it']('Value with \" inside')
            b.multiline(
              'text\n\tcan continue\nfor many "lines" of\n  possibly '
              'indented\n\ntext'
            )
            # Note invalid = unchanged.
            b.Escapes('\t \n \\d')
            with b.Oneliner:
                b.name('value')
        
        with b.CommentChecks:
            b['after ']('value')
            b.Flag('allowed')
            b.FlagAllows('This')
            b.Replaced('toreplace')
            b.Replaced('alsothis')
            b.Replaced('toreplace2')
            b.Replaced('alsothis2')
            with b.Replaced:
                b.lambda_('should')
                b.replace('above')
            with b.Replaced:
                b['lambda2']('should2')
                b.replace2('above2')

    assert_tree(parse_result, prop)
예제 #14
0
    def write_manifest(self) -> None:
        """Produce and pack a manifest file for this map.

        It will be packed such that it can override the master manifest with
        sv_soundemitter_flush.
        """
        manifest = Property('game_sounds_manifest', [
            Property('precache_file', snd)
            for snd, is_enabled in self.soundscript_files.items()
            if is_enabled is SoundScriptMode.INCLUDE
        ])

        buf = bytearray()
        for line in manifest.export():
            buf.extend(line.encode('utf8'))

        self.pack_file(
            'scripts/game_sounds_manifest.txt',
            FileType.SOUNDSCRIPT,
            bytes(buf),
        )
예제 #15
0
    def load_soundscript(
        self,
        file: File,
        *,
        always_include: bool = False,
    ) -> Iterable[str]:
        """Read in a soundscript and record which files use it.

        If always_include is True, it will be included in the manifests even
        if it isn't used.

        The sounds registered by this soundscript are returned.
        """
        with file.sys, file.open_str() as f:
            props = Property.parse(f, file.path)
        return self._parse_soundscript(props, file.path, always_include)
예제 #16
0
    def load_soundscript(
        self,
        file: File,
        *,
        always_include: bool=False,
    ) -> Iterable[str]:
        """Read in a soundscript and record which files use it.

        If always_include is True, it will be included in the manifests even
        if it isn't used.

        The sounds registered by this soundscript are returned.
        """
        with file.sys, file.open_str() as f:
            props = Property.parse(f, file.path)
        return self._parse_soundscript(props, file.path, always_include)
예제 #17
0
    def parse_manifest(fsys: FileSystem, file: File=None) -> Dict[str, 'SurfaceProp']:
        """Load surfaceproperties from a manifest.
        
        "scripts/surfaceproperties_manifest.txt" will be used if a file is
        not specified.
        """  
        with fsys:
            if not file:
                file = fsys['scripts/surfaceproperties_manifest.txt']
            
            with file.open_str() as f:
                manifest = Property.parse(f, file.path)
                
            surf = {}
            
            for prop in manifest.find_all('surfaceproperties_manifest', 'file'):
                surf = SurfaceProp.parse_file(fsys.read_prop(prop.value), surf)

            return surf
예제 #18
0
    def parse_manifest(fsys: FileSystem, file: File=None) -> Dict[str, 'SurfaceProp']:
        """Load surfaceproperties from a manifest.
        
        "scripts/surfaceproperties_manifest.txt" will be used if a file is
        not specified.
        """  
        with fsys:
            if not file:
                file = fsys['scripts/surfaceproperties_manifest.txt']
            
            with file.open_str() as f:
                manifest = Property.parse(f, file.path)
                
            surf = {}
            
            for prop in manifest.find_all('surfaceproperties_manifest', 'file'):
                surf = SurfaceProp.parse_file(fsys.read_prop(prop.value), surf)

            return surf
예제 #19
0
    def load_soundscript_manifest(self, cache_file: str=None) -> None:
        """Read the soundscript manifest, and read all mentioned scripts.

        If cache_file is provided, it should be a path to a file used to
        cache the file reading for later use.
        """
        try:
            man = self.fsys.read_prop('scripts/game_sounds_manifest.txt')
        except FileNotFoundError:
            return

        cache_data = {}  # type: Dict[str, Tuple[int, Property]]
        if cache_file is not None:
            try:
                f = open(cache_file)
            except FileNotFoundError:
                pass
            else:
                with f:
                    old_cache = Property.parse(f, cache_file)
                for cache_prop in old_cache:
                    cache_data[cache_prop.name] = (
                        cache_prop.int('cache_key'),
                        cache_prop.find_key('files')
                    )

            # Regenerate from scratch each time - that way we remove old files
            # from the list.
            new_cache_data = Property(None, [])
        else:
            new_cache_data = None

        with self.fsys:
            for prop in man.find_children('game_sounds_manifest'):
                if not prop.name.endswith('_file'):
                    continue
                try:
                    cache_key, cache_files = cache_data[prop.value.casefold()]
                except KeyError:
                    cache_key = -1
                    cache_files = None

                file = self.fsys[prop.value]
                cur_key = file.cache_key()

                if cache_key != cur_key or cache_key == -1:
                    sounds = self.load_soundscript(file, always_include=True)
                else:
                    # Read from cache.
                    sounds = []
                    for cache_prop in cache_files:
                        sounds.append(cache_prop.real_name)
                        self.soundscripts[cache_prop.real_name] = (prop.value, [
                            snd.value
                            for snd in cache_prop
                        ])

                # The soundscripts in the manifests are always included,
                # since many would be part of the core code (physics, weapons,
                # ui, etc). Just keep those loaded, no harm since vanilla does.
                self.soundscript_files[file.path] = SoundScriptMode.INCLUDE

                if new_cache_data is not None:
                    new_cache_data.append(Property(prop.value, [
                        Property('cache_key', str(cur_key)),
                        Property('Files', [
                            Property(snd, [
                                Property('snd', raw)
                                for raw in self.soundscripts[snd][1]
                            ])
                            for snd in sounds
                        ])
                    ]))

        if cache_file is not None:
            # Write back out our new cache with updated data.
            with open(cache_file, 'w') as f:
                for line in new_cache_data.export():
                    f.write(line)
예제 #20
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
예제 #21
0
    def pack_file(
        self,
        filename: str,
        data_type: FileType=FileType.GENERIC,
        data: bytes=None,
        skinset: Set[int]=None,
        optional: bool = False,
    ) -> None:
        """Queue the given file to be packed.

        If data is set, this file will use the given data instead of any
        on-disk data. The data_type parameter allows specifying the kind of
        file, which ensures it can be treated appropriately.

        If the file is a model, skinset allows restricting which skins are used.
        If None (default), all skins may be used. Otherwise it is a set of
        skins. If all uses of a model restrict the skin, only those skins need
        to be packed.
        If optional is set, this will be marked as optional so no errors occur
        if it isn't in the filesystem.
        """
        filename = os.fspath(filename)

        # Assume an empty filename is an optional value.
        if not filename:
            if data is not None:
                raise ValueError('Data provided with no filename!')
            return

        if '\t' in filename:
            raise ValueError(
                'No tabs are allowed in filenames ({!r})'.format(filename)
            )

        if data_type is FileType.GAME_SOUND:
            self.pack_soundscript(filename)
            return  # This packs the soundscript and wav for us.
        if data_type is FileType.PARTICLE:
            # self.pack_particle(filename)  # TODO: Particle parsing
            return  # This packs the PCF and material if required.
        if data_type is FileType.CHOREO:
            # self.pack_choreo(filename)  # TODO: Choreo scene parsing
            return

        # If soundscript data is provided, load it and force-include it.
        elif data_type is FileType.SOUNDSCRIPT and data:
            self._parse_soundscript(
                Property.parse(data.decode('utf8'), filename),
                filename,
                always_include=True,
            )

        filename = unify_path(filename)

        if data_type is FileType.MATERIAL or (
            data_type is FileType.GENERIC and filename.endswith('.vmt')
        ):
            data_type = FileType.MATERIAL
            if not filename.startswith('materials/'):
                filename = 'materials/' + filename
            if filename.endswith('.spr'):
                # This is really wrong, spr materials don't exist anymore.
                # Silently swap the extension.
                filename = filename[:-3] + 'vmt'
            elif not filename.endswith('.vmt'):
                filename = filename + '.vmt'
        elif data_type is FileType.TEXTURE or (
            data_type is FileType.GENERIC and filename.endswith('.vtf')
        ):
            data_type = FileType.TEXTURE
            if not filename.startswith('materials/'):
                filename = 'materials/' + filename
            if not filename.endswith('.vtf'):
                filename = filename + '.vtf'
        elif data_type is FileType.VSCRIPT_SQUIRREL or (
            data_type is FileType.GENERIC and filename.endswith('.nut')
        ):
            data_type = FileType.VSCRIPT_SQUIRREL
            if not filename.endswith('.nut'):
                filename = filename + '.nut'

        if data_type is FileType.MODEL or filename.endswith('.mdl'):
            data_type = FileType.MODEL
            if not filename.startswith('models/'):
                filename = 'models/' + filename
            if not filename.endswith('.mdl'):
                filename = filename + '.mdl'
            if skinset is None:
                # It's dynamic, this overrides any previous specific skins.
                self.skinsets[filename] = None
            else:
                try:
                    existing_skins = self.skinsets[filename]
                except KeyError:
                    self.skinsets[filename] = skinset.copy()
                else:
                    # Merge the two.
                    if existing_skins is not None:
                        self.skinsets[filename] = existing_skins | skinset

        try:
            file = self._files[filename]
        except KeyError:
            pass  # We need to make it.
        else:
            # It's already here, is that OK?

            # Allow overriding data on disk with ours..
            if file.data is None:
                if data is not None:
                    file.data = data
                # else: no data on either, that's fine.
            elif data == file.data:
                pass  # Overrode with the same data, that's fine
            elif data:
                raise ValueError('"{}": two different data streams!'.format(filename))
            # else: we had an override, but asked to just pack now. That's fine.

            # Override optional packing with required packing.
            if not optional:
                file.optional = False

            if file.type is data_type:
                # Same, no problems - just packing on top.
                return

            if file.type is FileType.GENERIC:
                file.type = data_type  # This is fine, we now know it has behaviour...
            elif data_type is FileType.GENERIC:
                pass  # If we know it has behaviour already, that trumps generic.
            else:
                raise ValueError('"{}": {} can\'t become a {}!'.format(
                    filename,
                    file.type.name,
                    data_type.name,
                ))
            return  # Don't re-add this.

        start, ext = os.path.splitext(filename)

        # Try to promote generic to other types if known.
        if data_type is FileType.GENERIC:
            try:
                data_type = EXT_TYPE[ext]
            except KeyError:
                pass
        elif data_type is FileType.SOUNDSCRIPT:
            if ext != '.txt':
                raise ValueError('"{}" cannot be a soundscript!'.format(filename))

        self._files[filename] = PackFile(data_type, filename, data, optional)
예제 #22
0
    def load_soundscript_manifest(self, cache_file: str = None) -> None:
        """Read the soundscript manifest, and read all mentioned scripts.

        If cache_file is provided, it should be a path to a file used to
        cache the file reading for later use.
        """
        try:
            man = self.fsys.read_prop('scripts/game_sounds_manifest.txt')
        except FileNotFoundError:
            return

        cache_data = {}  # type: Dict[str, Tuple[int, Property]]
        if cache_file is not None:
            try:
                f = open(cache_file)
            except FileNotFoundError:
                pass
            else:
                with f:
                    old_cache = Property.parse(f, cache_file)
                for cache_prop in old_cache:
                    cache_data[cache_prop.name] = (
                        cache_prop.int('cache_key'),
                        cache_prop.find_key('files'))

            # Regenerate from scratch each time - that way we remove old files
            # from the list.
            new_cache_data = Property(None, [])
        else:
            new_cache_data = None

        with self.fsys:
            for prop in man.find_children('game_sounds_manifest'):
                if not prop.name.endswith('_file'):
                    continue
                try:
                    cache_key, cache_files = cache_data[prop.value.casefold()]
                except KeyError:
                    cache_key = -1
                    cache_files = None

                file = self.fsys[prop.value]
                cur_key = file.cache_key()

                if cache_key != cur_key or cache_key == -1:
                    sounds = self.load_soundscript(file, always_include=True)
                else:
                    # Read from cache.
                    sounds = []
                    for cache_prop in cache_files:
                        sounds.append(cache_prop.real_name)
                        self.soundscripts[cache_prop.real_name] = (
                            prop.value, [snd.value for snd in cache_prop])

                # The soundscripts in the manifests are always included,
                # since many would be part of the core code (physics, weapons,
                # ui, etc). Just keep those loaded, no harm since vanilla does.
                self.soundscript_files[file.path] = SoundScriptMode.INCLUDE

                if new_cache_data is not None:
                    new_cache_data.append(
                        Property(prop.value, [
                            Property('cache_key', str(cur_key)),
                            Property('Files', [
                                Property(snd, [
                                    Property('snd', raw)
                                    for raw in self.soundscripts[snd][1]
                                ]) for snd in sounds
                            ])
                        ]))

        if cache_file is not None:
            # Write back out our new cache with updated data.
            with open(cache_file, 'w') as f:
                for line in new_cache_data.export():
                    f.write(line)
예제 #23
0
    def load_soundscript_manifest(self, cache_file: str=None) -> None:
        """Read the soundscript manifest, and read all mentioned scripts.

        If cache_file is provided, it should be a path to a file used to
        cache the file reading for later use. 
        """
        try:
            man = self.fsys.read_prop('scripts/game_sounds_manifest.txt')
        except FileNotFoundError:
            return

        cache_data = {}  # type: Dict[str, Tuple[int, Property]]
        if cache_file is not None:
            # If the file doesn't exist or is corrupt, that's
            # fine. We'll just parse the soundscripts the slow
            # way.
            try:
                with open(cache_file) as f:
                    old_cache = Property.parse(f, cache_file)
                if man['version'] != SOUND_CACHE_VERSION:
                    raise LookupError
            except (FileNotFoundError, KeyValError, LookupError):
                pass
            else:
                for cache_prop in old_cache.find_children('Sounds'):
                    cache_data[cache_prop.name] = (
                        cache_prop.int('cache_key'),
                        cache_prop.find_key('files')
                    )

            # Regenerate from scratch each time - that way we remove old files
            # from the list.
            new_cache_sounds = Property('Sounds', [])
            new_cache_data = Property(None, [
                Property('version', SOUND_CACHE_VERSION),
                new_cache_sounds,
            ])
        else:
            new_cache_data = new_cache_sounds = None

        with self.fsys:
            for prop in man.find_children('game_sounds_manifest'):
                if not prop.name.endswith('_file'):
                    continue
                try:
                    cache_key, cache_files = cache_data[prop.value.casefold()]
                except KeyError:
                    cache_key = -1
                    cache_files = None

                try:
                    file = self.fsys[prop.value]
                except FileNotFoundError:
                    LOGGER.warning('Soundscript "{}" does not exist!', prop.value)
                    # Don't write anything into the cache, so we check this
                    # every time.
                    continue
                cur_key = file.cache_key()

                if cache_key != cur_key or cache_key == -1:
                    sounds = self.load_soundscript(file, always_include=True)
                else:
                    # Read from cache.
                    sounds = []
                    for cache_prop in cache_files:
                        sounds.append(cache_prop.real_name)
                        self.soundscripts[cache_prop.real_name] = (prop.value, [
                            snd.value
                            for snd in cache_prop
                        ])

                # The soundscripts in the manifests are always included,
                # since many would be part of the core code (physics, weapons,
                # ui, etc). Just keep those loaded, no harm since vanilla does.
                self.soundscript_files[file.path] = SoundScriptMode.INCLUDE

                if new_cache_sounds is not None:
                    new_cache_sounds.append(Property(prop.value, [
                        Property('cache_key', str(cur_key)),
                        Property('Files', [
                            Property(snd, [
                                Property('snd', raw)
                                for raw in self.soundscripts[snd][1]
                            ])
                            for snd in sounds
                        ])
                    ]))

        if cache_file is not None:
            # Write back out our new cache with updated data.
            with srctools.AtomicWriter(cache_file) as f:
                for line in new_cache_data.export():
                    f.write(line)
예제 #24
0
    def pack_file(
        self,
        filename: str,
        data_type: FileType = FileType.GENERIC,
        data: bytes = None,
    ) -> None:
        """Queue the given file to be packed.

        If data is set, this file will use the given data instead of any
        on-disk data. The data_type parameter allows specifying the kind of
        file, which ensures it can be treated appropriately.
        """
        filename = os.fspath(filename)

        if '\t' in filename:
            raise ValueError(
                'No tabs are allowed in filenames ({!r})'.format(filename))

        if data_type is FileType.GAME_SOUND:
            self.pack_soundscript(filename)
            return  # This packs the soundscript and wav for us.

        # If soundscript data is provided, load it and force-include it.
        elif data_type is FileType.SOUNDSCRIPT and data:
            self._parse_soundscript(
                Property.parse(data.decode('utf8'), filename),
                filename,
                always_include=True,
            )

        if data_type is FileType.MATERIAL or (data_type is FileType.GENERIC
                                              and filename.endswith('.vmt')):
            data_type = FileType.MATERIAL
            if not filename.startswith('materials/'):
                filename = 'materials/' + filename
            if not filename.endswith(('.vmt', '.spr')):
                filename = filename + '.vmt'
        elif data_type is FileType.TEXTURE or (data_type is FileType.GENERIC
                                               and filename.endswith('.vtf')):
            data_type = FileType.TEXTURE
            if not filename.startswith('materials/'):
                filename = 'materials/' + filename
            if not filename.endswith('.vtf'):
                filename = filename + '.vtf'

        path = unify_path(filename)

        try:
            file = self._files[path]
        except KeyError:
            pass  # We need to make it.
        else:
            # It's already here, is that OK?

            # Allow overriding data on disk with ours..
            if file.data is None:
                if data is not None:
                    file.data = data
                # else: no data on either, that's fine.
            elif data == file.data:
                pass  # Overrode with the same data, that's fine
            elif data:
                raise ValueError(
                    '"{}": two different data streams!'.format(filename))
            # else: we had an override, but asked to just pack now. That's fine.

            if file.type is data_type:
                # Same, no problems - just packing on top.
                return

            if file.type is FileType.GENERIC:
                file.type = data_type  # This is fine, we now know it has behaviour...
            elif data_type is FileType.GENERIC:
                pass  # If we know it has behaviour already, that trumps generic.
            elif data_type is FileType.WHITELIST:
                file.type = data_type  # Blindly believe this.
            else:
                raise ValueError('"{}": {} can\'t become a {}!'.format(
                    filename,
                    file.type.name,
                    data_type.name,
                ))
            return  # Don't re-add this.

        start, ext = os.path.splitext(path)

        # Try to promote generic to other types if known.
        if data_type is FileType.GENERIC:
            try:
                data_type = EXT_TYPE[ext]
            except KeyError:
                pass
        elif data_type is FileType.SOUNDSCRIPT:
            if ext != '.txt':
                raise ValueError(
                    '"{}" cannot be a soundscript!'.format(filename))

        self._files[path] = PackFile(
            data_type,
            filename,
            data,
        )
예제 #25
0
def test_build_exc() -> None:
    """Test the with statement handles exceptions correctly."""
    class Exc(Exception):
        pass

    prop = Property('Root', [])

    with pytest.raises(Exc):  # Should not swallow.
        with prop.build() as build:
            build.prop('Hi')
            raise Exc
    # Exception doesn't rollback.
    assert_tree(Property('Root', [
        Property('prop', 'Hi'),
    ]), prop)

    prop.clear()

    with prop.build() as build:
        build.leaf('value')
        with pytest.raises(Exc):
            with build.subprop as sub:
                raise Exc
    assert_tree(
        Property('Root', [
            Property('leaf', 'value'),
            Property('subprop', []),
        ]), prop)
예제 #26
0
def test_blockfuncs_fail_on_leaf() -> None:
    """Check that methods requiring a block fail on a leaf property."""
    leaf = Property('Name', 'blah')
    with pytest.raises(ValueError):
        for _ in leaf.find_all("blah"):
            pass
    with pytest.raises(ValueError):
        leaf.find_key("blah")
    with pytest.raises(ValueError):
        for _ in leaf:
            pass
    with pytest.raises(ValueError):
        leaf['blah']
    with pytest.raises(ValueError):
        leaf['blah'] = 't'
    with pytest.raises(ValueError):
        leaf.int('blah')
    with pytest.raises(ValueError):
        leaf.bool('blah')
    with pytest.raises(ValueError):
        leaf.float('blah')
    with pytest.raises(ValueError):
        leaf.vec('blah')
    with pytest.raises(ValueError):
        len(leaf)
    with pytest.raises(ValueError):
        with leaf.build():
            pass
    with pytest.raises(ValueError):
        leaf.ensure_exists('blah')
    with pytest.raises(ValueError):
        leaf.set_key(("blah", "another"), 45)
    with pytest.raises(ValueError):
        leaf.merge_children()
def test_edit():
    """Check functionality of Property.edit()"""
    test_prop = Property('Name', 'Value')

    def check(prop: Property, name, value):
        """Check the property was edited, and has the given value."""
        nonlocal test_prop
        assert prop is test_prop
        assert prop.real_name == name
        assert prop.value == value
        test_prop = Property('Name', 'Value')

    check(test_prop.edit(), 'Name', 'Value')
    check(test_prop.edit(name='new_name',), 'new_name', 'Value')
    check(test_prop.edit(value='new_value'), 'Name', 'new_value')

    # Check converting a block into a keyvalue
    test_prop = Property('Name', [
        Property('Name', 'Value')
    ])
    check(test_prop.edit(value='Blah'), 'Name', 'Blah')

    # Check converting a keyvalue into a block.
    child_1 = Property('Key', 'Value')
    new_prop = test_prop.edit(value=[child_1, Property('Key2', 'Value')])
    assert test_prop is new_prop
    assert list(test_prop)[0] is child_1
예제 #28
0
    def pack_file(
        self,
        filename: str,
        data_type: FileType=FileType.GENERIC,
        data: bytes=None,
    ) -> None:
        """Queue the given file to be packed.

        If data is set, this file will use the given data instead of any
        on-disk data. The data_type parameter allows specifying the kind of
        file, which ensures it can be treated appropriately.
        """
        filename = os.fspath(filename)

        if '\t' in filename:
            raise ValueError(
                'No tabs are allowed in filenames ({!r})'.format(filename)
            )

        if data_type is FileType.GAME_SOUND:
            self.pack_soundscript(filename)
            return  # This packs the soundscript and wav for us.

        # If soundscript data is provided, load it and force-include it.
        elif data_type is FileType.SOUNDSCRIPT and data:
            self._parse_soundscript(
                Property.parse(data.decode('utf8'), filename),
                filename,
                always_include=True,
            )

        if data_type is FileType.MATERIAL or (
            data_type is FileType.GENERIC and filename.endswith('.vmt')
        ):
            data_type = FileType.MATERIAL
            if not filename.startswith('materials/'):
                filename = 'materials/' + filename
            if not filename.endswith(('.vmt', '.spr')):
                filename = filename + '.vmt'
        elif data_type is FileType.TEXTURE or (
            data_type is FileType.GENERIC and filename.endswith('.vtf')
        ):
            data_type = FileType.TEXTURE
            if not filename.startswith('materials/'):
                filename = 'materials/' + filename
            if not filename.endswith('.vtf'):
                filename = filename + '.vtf'

        path = unify_path(filename)

        try:
            file = self._files[path]
        except KeyError:
            pass  # We need to make it.
        else:
            # It's already here, is that OK?

            # Allow overriding data on disk with ours..
            if file.data is None:
                if data is not None:
                    file.data = data
                # else: no data on either, that's fine.
            elif data == file.data:
                pass  # Overrode with the same data, that's fine
            elif data:
                raise ValueError('"{}": two different data streams!'.format(filename))
            # else: we had an override, but asked to just pack now. That's fine.

            if file.type is data_type:
                # Same, no problems - just packing on top.
                return

            if file.type is FileType.GENERIC:
                file.type = data_type  # This is fine, we now know it has behaviour...
            elif data_type is FileType.GENERIC:
                pass  # If we know it has behaviour already, that trumps generic.
            elif data_type is FileType.WHITELIST:
                file.type = data_type  # Blindly believe this.
            else:
                raise ValueError('"{}": {} can\'t become a {}!'.format(
                    filename,
                    file.type.name,
                    data_type.name,
                ))
            return  # Don't re-add this.

        start, ext = os.path.splitext(path)

        # Try to promote generic to other types if known.
        if data_type is FileType.GENERIC:
            try:
                data_type = EXT_TYPE[ext]
            except KeyError:
                pass
        elif data_type is FileType.SOUNDSCRIPT:
            if ext != '.txt':
                raise ValueError('"{}" cannot be a soundscript!'.format(filename))

        self._files[path] = PackFile(
            data_type,
            filename,
            data,
        )
         P('Empty', []),
     ]),
     P('Block', [
         P('bare', [
             P('block', 'he\tre'),
         ]),
     ]),
 ]),
 P('Root2', [
     P('Name with " in it', 'Value with \" inside'),
     P('multiline',
       'text\n\tcan continue\nfor many "lines" of\n  possibly indented\n\ntext'
       ),
     # Note, invalid = unchanged.
     P('Escapes', '\t \n \\d'),
     P('Oneliner', [Property('name', 'value')]),
 ]),
 P('CommentChecks', [
     P('after ', 'value'),
     P('Flag', 'allowed'),
     P('FlagAllows', 'This'),
     P('Replaced', 'toreplace'),
     P('Replaced', 'alsothis'),
     P('Replaced', 'toreplace2'),
     P('Replaced', 'alsothis2'),
     P('Replaced', [
         P('lambda', 'should'),
         P('replace', 'above'),
     ]),
     P('Replaced', [
         P('lambda2', 'should2'),