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
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
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')
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 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 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), )
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)
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
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)
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
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)
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'),
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)