예제 #1
0
def Delete_Faction_Logic(empty_diffs = 0):
    '''
    Clears out faction logic cues entirely.
    Test result: no change.
    '''
    '''
    Testing using same setup as above (eg. 3x jobs):
    - Baseline: 43.4 fps
    - Changed: 45.8 fps
    '''
    md_files = Load_Files('md/faction*.xml')
    
    for game_file in md_files:
        xml_root = game_file.Get_Root()

        # Do this on every cue and library, top level.
        changed = False
        for tag in ['cue', 'library']:
            nodes = xml_root.xpath("./cues/{}".format(tag))
            if not nodes:
                continue

            changed = True
            if empty_diffs:
                continue

            for node in nodes:
                # Delete this node.
                node.getparent().remove(node)

        if changed:
            # Commit changes right away; don't bother delaying for errors.
            game_file.Update_Root(xml_root)

    return
예제 #2
0
def Remove_Dock_Glow():
    '''
    Removes the glow effect from station docks.
    '''
    # Find every "dockarea" file of interest, using a wildcard pattern.
    # This will catch the vanilla docks. The path can be modified if
    # needing to catch other name patterns from extensions.
    dock_files = Load_Files('*dockarea_arg_m_station*.xml')

    for game_file in dock_files:
        # Extract an editable version of its xml.
        xml_root = game_file.Get_Root()

        # Do an xpath search to find xml elements.
        # This looks for the fx_glow parts.
        results = xml_root.xpath(".//connection[parts/part/@name='fx_glow']")
        # Skip if none found.
        if not results:
            continue

        # Loop over the connection elements.
        for conn in results:
            # Remove it from its parent.
            # (lxml syntax)
            conn.getparent().remove(conn)

        # Now that the xml has been edited, and didn't have an error,
        # apply it back to the game_file.
        game_file.Update_Root(xml_root)
    return
예제 #3
0
def Run():
    '''
    Setup the customized scripts.
    '''
    # Load settings from the ini file(s).
    # Defaults are in settings_defaults.ini
    # User overrides are in settings.ini (may or may not exist).
    config = configparser.ConfigParser()
    config.read([this_dir / 'config_defaults.ini', this_dir / 'config.ini'])

    # Set customizer settings.
    Settings(
        # Generate the extension here.
        path_to_output_folder=this_dir.parent,
        extension_name=this_dir.name,
        developer=True,
    )

    # Set the path to the X4 installation folder.
    if config['General']['x4_path']:
        Settings(path_to_x4_folder=config['General']['x4_path'])

    # Evaluate the patterns to collect all files.
    game_files = []
    for field, pattern in config['Scripts'].items():

        # Make sure the pattern ends in xml.
        if not pattern.endswith('.xml'):
            pattern += '.xml'

        # Filter out duplicates (generally not expected, but can happen).
        for file in Load_Files(pattern):
            if file not in game_files:
                game_files.append(file)

    # Separate into aiscript and md files.
    ai_files = []
    md_files = []
    for file in game_files:
        path = file.virtual_path
        if '/' not in path:
            continue
        folder = path.split('/')[-2]
        if folder == 'aiscripts':
            ai_files.append(file)
        elif folder == 'md':
            md_files.append(file)

    # Hand off to helper functions.
    Annotate_Scripts(ai_files, style='ai')
    Annotate_Scripts(md_files, style='md')

    # Ensure any extensions being modified are set as dependencies.
    Update_Content_XML_Dependencies()
    Write_To_Extension(skip_content=True)
    return
예제 #4
0
def Adjust_OOS_Damage(multiplier):
    '''
    Adjusts all out-of-vision damage-per-second by a multiplier. For instance,
    if OOS combat seems to run too fast, it can be multiplied by 0.5 to
    slow it down by half.
    
    * multiplier
      - Float, how much to multiply damage by.
    '''
    # The code for this is similar to what was done above, just split off.
    # If the two transforms are used together, it's probably okay, will
    # just have a few extra cheap script instructions.

    aiscript_files = Load_Files(f"*aiscripts/*.xml")

    for game_file in aiscript_files:
        xml_root = game_file.Get_Root()
        file_name = game_file.name.replace('.xml', '')

        # Find any get_attack_strength nodes, used in OOS combat.
        nodes = xml_root.xpath(".//get_attackstrength")
        if not nodes:
            continue

        for node in nodes:

            # Gather the names of capture vars.
            out_vars = []
            if node.get('result'):
                out_vars.append(node.get('result'))

            result_subnode = node.find('./result')
            if result_subnode != None:
                for attr in [
                        'hullshield', 'hullonly', 'shieldonly', 'hullnoshield'
                ]:
                    varname = result_subnode.get(attr)
                    if varname:
                        out_vars.append(varname)

            # Add in set_value nodes to multiply each of these.
            for varname in out_vars:
                new_node = Element('set_value',
                                   name=varname,
                                   exact=f'{varname} * {multiplier}')
                node.addnext(new_node)
                # lxml can mess up node id tails, so fix it.
                if new_node.tail != None:
                    assert node.tail == None
                    node.tail = new_node.tail
                    new_node.tail = None

        game_file.Update_Root(xml_root)

    return
예제 #5
0
def Simpler_Moves(empty_diffs = 0):
    '''
    Change all moves to use a linear flight model.
    '''
    '''
    Test on 20k ships save above trinity sanctum.
    without edit: 32 fps (reusing above)
    with edit   : 30.5 fps (probably in the noise).
    No real difference.
    '''
    
    '''
    Unitrader suggestion:
    "<set_flight_control_model object="this.ship" flightcontrolmodel="flightcontrolmodel.linear"/> 
    before every movement command (and remove all @forcesteering if present)"
    '''

    aiscript_files = Load_Files('aiscripts/*.xml')
    #aiscript_files = [Load_File('aiscripts/order.fight.escort.xml')]
    
    for game_file in aiscript_files:
        xml_root = game_file.Get_Root()
        file_name = game_file.name.replace('.xml','')
        
        if not empty_diffs:
            # All moves.
            for tag in [
                'move_approach_path',
                'move_docking',
                'move_undocking',
                'move_gate',
                'move_navmesh',
                'move_strafe',
                'move_target_points',
                'move_waypoints',
                'move_to',
                ]:
                nodes = xml_root.xpath(".//{}".format(tag))
                if not nodes:
                    continue

                for node in nodes:
                    # Prepend with the set_flight_control_model.
                    flight_node = Element(
                        'set_flight_control_model', 
                        object = "this.ship",
                        flightcontrolmodel = "flightcontrolmodel.linear")
                    node.set('forcesteering', 'false')
                    node.addprevious(flight_node)
                    assert not flight_node.tail
                    
        game_file.Update_Root(xml_root)
                    
    return
예제 #6
0
def Increase_Waits(multiplier = 10, filter = '*', empty_diffs = 0):
    '''
    Multiply the duration of all wait statements.

    * multiplier
      - Float, factor to increase wait times by.
    * filter
      - String, possibly with wildcards, matching names of ai scripts to
        modify; default is plain '*' to match all aiscripts.
    '''
    '''
    Test on 20k ships save above trinity sanctum.
    without edit   : 32 fps (reusing above; may be low?)
    with 10x       : 50 fps (immediate benefit)
    just trade 10x : 37 fps
    with 2x        : 46 fps

    Success!  (idea: can also scale wait by if seta is active)
    '''
    
    # Just ai scripts; md has no load.
    aiscript_files = Load_Files(f'aiscripts/{filter}.xml')
    #aiscript_files = [Load_File('aiscripts/order.fight.escort.xml')]
    
    for game_file in aiscript_files:
        xml_root = game_file.Get_Root()
        file_name = game_file.name.replace('.xml','')
        
        if not empty_diffs:
            nodes = xml_root.xpath(".//wait")
            if not nodes:
                continue

            for node in nodes:
                for attr in ['min','max','exact']:
                    orig = node.get(attr)
                    if orig:
                        # Wrap the old value or expression, and multiply.
                        node.set(attr, f'({node.get(attr)})*{multiplier}')
                    
        game_file.Update_Root(xml_root)
                    
    return
예제 #7
0
def Disable_AI_Travel_Drive():
    '''
    Disables usage of travel drives for all ai scripts. When applied to
    a save, existing move orders may continue to use travel drive
    until they complete.
    '''
    # Travel drive is part of the move commands, as one of the arguments.
    # Can set to false always, to disable use.
    # (This appears to only apply to plain move_to?)

    aiscript_files = Load_Files(f"*aiscripts/*.xml")

    for game_file in aiscript_files:
        xml_root = game_file.Get_Root()
        file_name = game_file.name.replace('.xml', '')

        change_occurred = False
        for tag in [
                #'move_approach_path',
                #'move_docking',
                #'move_undocking',
                #'move_gate',
                #'move_navmesh',
                #'move_strafe',
                #'move_target_points',
                #'move_waypoints',
                'move_to',
        ]:
            nodes = xml_root.xpath(".//{}".format(tag))
            if not nodes:
                continue

            for node in nodes:
                # Check if this uses the travel arg; if not, it defaults to
                # false, so no change needed.
                if node.get('travel') != None:
                    node.set('travel', 'false')
                    change_occurred = True

        if change_occurred:
            game_file.Update_Root(xml_root)

    return
예제 #8
0
def Remove_Moves(empty_diffs = 0):
    '''
    Replace all move commands with waits.
    '''
    '''
    Test on 20k ships save above trinity sanctum.
    without edit: 32 fps
    with edit   : 45->~60 fps (gradually climbing over time)~
    '''
    aiscript_files = Load_Files('aiscripts/*.xml')
    #aiscript_files = [Load_File('aiscripts/order.fight.escort.xml')]
    
    for game_file in aiscript_files:
        xml_root = game_file.Get_Root()
        file_name = game_file.name.replace('.xml','')
        
        if not empty_diffs:
            # All moves.
            for tag in [
                'move_approach_path',
                'move_docking',
                'move_undocking',
                'move_gate',
                'move_navmesh',
                'move_strafe',
                'move_target_points',
                'move_waypoints',
                'move_to',
                ]:
                nodes = xml_root.xpath(".//{}".format(tag))
                if not nodes:
                    continue

                for node in nodes:
                    # Create a wait node, of some significant duration.
                    wait = Element('wait', exact='30s')
                    node.getparent().replace(node, wait)
                    assert not wait.tail
                    
        game_file.Update_Root(xml_root)
                    
    return
예제 #9
0
def Remove_Dock_Glow():
    '''
    Removes the glow effect from station docks.
    '''
    # Find every "dockarea" file.
    dock_files = Load_Files('*dockarea_arg_m_station*.xml')
    '''
    Of interest are the connections that define the parts for the fx glow/haze.
    Examples:
        ' <connection name="Connection04" tags="part detail_xl nocollision fx  ">
        '   ...
        '   <parts>
        '     <part name="fx_haze">
        '   ...
    and
        ' <connection name="Connection02" tags="part detail_l nocollision fx  ">
        '   ...
        '   <parts>
        '     <part name="fx_glow">

    In testing:
        Glow is the giant blue ball effect.
        Haze is a greyish fog close to the platform.
    Remove just glow.
    '''

    for game_file in dock_files:
        xml_root = game_file.Get_Root()

        results = xml_root.xpath(".//connection[parts/part/@name='fx_glow']")
        if not results:
            continue

        for conn in results:
            # Remove it from its parent.
            conn.getparent().remove(conn)

        # Commit changes right away; don't bother delaying for errors.
        game_file.Update_Root(xml_root)
    return
예제 #10
0
def Annotate_MD_Scripts(empty_diffs = 0):
    '''
    For every md script, add performance measuring nodes.
    Test result: ego md only uses ~0.5% of the compute time, so
    not of particular potential for optimization.
    This may vary with other setups or extensions.
    '''
    '''
    Basic idea:
    - At various points, gather a time sample.
    - Pass samples to lua.
    - Separate lua script computes time deltas between points, sums
      path delays and hits (and hits at gather points), etc.
    '''
    # TODO: maybe include extension files in general.
    md_files = Load_Files('md/*.xml')
        
    for game_file in md_files:
        xml_root = game_file.Get_Root()
        file_name = game_file.name.replace('.xml','')

        # Gather a list of sample points.
        # These will be tuples of 
        # (section_name, location_name, path_bound "entry"/"mid"/"exit", node, 
        #  "before"/"after"/"firstchild"/"lastchild").
        # Where section_name should have the file_name, and a suffix
        # for the cue/lib involved, so that timers can be matched up
        # across lib calls from a cue.
        sample_points = []
        
        # Entering/exiting any action block. Include cue/lib name.
        actions_nodes = xml_root.xpath('.//actions')
        for actions_node in actions_nodes:
            cue_name = actions_node.getparent().get('name')

            # Skip if empty.
            if len(actions_node) == 0:
                continue

            # Pick out the entry/exit lines for annotation.
            first_line = Get_First_Source_Line(actions_node[0])
            last_line  = Get_Last_Source_Line(actions_node[-1])

            # Entry/exit redundant for now, but will differ for
            # specific nodes.
            Insert_Timestamp(f'md.{file_name}.{cue_name}', f'entry {first_line}', 'entry', actions_node, 'firstchild')
            Insert_Timestamp(f'md.{file_name}.{cue_name}', f'exit {last_line}'  , 'exit' , actions_node, 'lastchild')
                

        # -Removed, old style just counted visits, with no timing.
        ## Do this on every cue and library, including nestings.
        ## TODO: also flag every major control flow fork/join, eg. inside
        ## each do_if/else path and just after join.
        #changed = False
        #for tag in ['cue', 'library']:
        #    nodes = xml_root.xpath(".//{}".format(tag))
        #    if not nodes:
        #        continue
        #
        #    changed = True
        #    if empty_diffs:
        #        continue
        #
        #    for node in nodes:
        #
        #        # This doesn't really need to know sourceline, but include
        #        # it if handy.
        #        if node.sourceline:
        #            line_num = node.sourceline
        #        else:
        #            # In this case, it is likely from a diff patch.
        #            line_num = ''
        #
        #        # Include: file name, cue/lib name, line.
        #        # Update: gets too cluttered; just do file and cue/lib name.
        #        name_line = "'${} {}'".format(
        #            game_file.name.replace('.xml',''), 
        #            #tag,
        #            node.get('name'),
        #            #node.sourceline,
        #            )
        #
        #        # Need to add subcues to actions.
        #        actions = node.find('./actions')
        #        # If there are no actions for some reason, eg. it is
        #        # the parent of some child cues, then skip.
        #        if actions == None:
        #            continue
        #
        #        # Set up the counter.
        #        record_group = [
        #            etree.fromstring('''
        #                <do_if value="not md.SN_Measure_Perf.Globals.$md_cue_hits?">
        #                    <set_value name="md.SN_Measure_Perf.Globals.$md_cue_hits" exact="table[]"/>
        #                </do_if>'''),
        #            # Can accumulate a long time, so use a float or long int.
        #            etree.fromstring('''
        #                <do_if value="not md.SN_Measure_Perf.Globals.$md_cue_hits.{FIELD}?">
        #                    <set_value name="md.SN_Measure_Perf.Globals.$md_cue_hits.{FIELD}" exact="0"/>
        #                </do_if>'''.replace('FIELD', name_line)),
        #            etree.fromstring(('''
        #                <set_value name="md.SN_Measure_Perf.Globals.$md_cue_hits.{FIELD}" operation="add"/>'''
        #                ).replace('FIELD', name_line)),
        #        ]
        #        for record_node in record_group:
        #            actions.append(record_node)

        # Commit changes right away; don't bother delaying for errors.
        game_file.Update_Root(xml_root)
    return
예제 #11
0
def Annotate_AI_Scripts(empty_diffs = 0):
    '''
    For every ai script, add timestamps at entry, exit, and blocking nodes.
    Test result: ego ai scripts take ~6% of computation, at least for
    the bodies, with a vanilla 10hr save. Condition checks not captured.
    '''
    aiscript_files = Load_Files('aiscripts/*.xml')
    #aiscript_files = [Load_File('aiscripts/masstraffic.watchdog.xml')]
    
    for game_file in aiscript_files:
        xml_root = game_file.Get_Root()
        file_name = game_file.name.replace('.xml','')
        
        if not empty_diffs:
            # All normal blocking nodes (return to this script when done).
            for tag in [
                'dock_masstraffic_drone',
                'execute_custom_trade',
                'execute_trade',
                'move_approach_path',
                'move_docking',
                'move_undocking',
                'move_gate',
                'move_navmesh',
                'move_strafe',
                'move_target_points',
                'move_waypoints',
                'move_to',
                'detach_from_masstraffic',
                'run_script',
                'run_order_script',
                'wait_for_prev_script',
                'wait',
                ]:
                nodes = xml_root.xpath(".//{}".format(tag))
                if not nodes:
                    continue

                # All of these nodes need timestamps on both sides.
                for node in nodes:
                    # Pick out the entry/exit lines for annotation.
                    # (Do these nodes even have children?  Maybe; be safe.)
                    first_line = Get_First_Source_Line(node)
                    last_line  = Get_Last_Source_Line(node)

                    # Above the node exits a path; below the node enters a path.
                    Insert_Timestamp(
                        f'ai.{file_name}', 
                        f'{node.tag} {first_line}', 
                        'exit', node, 'before')
                    Insert_Timestamp(
                        f'ai.{file_name}', 
                        f'{node.tag} {last_line}', 
                        'entry' , node, 'after')
                

            # Special exit points.
            # Script can hard-return with a return node.
            for tag in ['return']:
                nodes = xml_root.xpath(".//{}".format(tag))
                if not nodes:
                    continue

                # Just timestamp the visit.
                for node in nodes:
                    first_line = Get_First_Source_Line(node)
                    Insert_Timestamp(
                        f'ai.{file_name}', 
                        f'{node.tag} {first_line}', 
                        'exit', node, 'before')


            # TODO:
            # Possible mid points:
            # -label
            # -resume
            # 

            # Blocks of actions can show up in:
            # -attention (one actions child)
            # -libraries (multiple actions children possible, each named)
            # -interrupts (may or may not have an actions block)
            # -handler (one block of actions, no name on this or handler)
            # -on_attentionchange (in theory; no examples seen)
            # Of these, all but libraries should start/end paths.
        
            # init blocks also have actions, though not labelled as such.
            # on_abort is similar.
            for tag in ['actions','init','on_abort']:
                nodes = xml_root.xpath(f'.//{tag}')

                for node in nodes:
                    # Skip if empty.
                    if len(node) == 0:
                        continue

                    # Skip if action parent is a libary.
                    # TODO: look into if libs can have blocking actions; if not,
                    # can set these up with a separate category name.
                    if tag == 'actions' and node.getparent().tag == 'library':
                        continue

                    # Pick out the entry/exit lines for annotation.
                    first_line = Get_First_Source_Line(node[0])
                    last_line  = Get_Last_Source_Line(node[-1])

                    Insert_Timestamp(
                        f'ai.{file_name}', 
                        f'{node.tag} entry {first_line}', 
                        'entry', node, 'firstchild')
                    Insert_Timestamp(
                        f'ai.{file_name}', 
                        f'{node.tag} exit {last_line}', 
                        'exit' , node, 'lastchild')

        game_file.Update_Root(xml_root)
    return
예제 #12
0
def Fadein_Asteroids(empty_diffs=0):
    '''
    Uniquify asteroid materials, and set fade-in rules to match when they
    first start drawing, for a smoother fade-in period.
    '''
    '''
    Asteroids are selected from their component files, often with multiple
    variations of the same size category (eg. large, medium, etc.).
    Size is defined in the component as part of the connection to a material.

    Different sizes asteroids often share the same material entry.
    Asteroid appearance rules are set by region_lodvalues.xml, which defines
    when the asteroid spawns and (slightly closer) when it starts drawing
    based on asteroid size.

    Asteroid material refs in the xml are dummies; the actual material is
    defined in the xmf binary, which needs a binary patch.
    Uniquified materials will use names of the same length, for easy patching.
    '''

    # Gather all of the asteroid components.
    # TODO: maybe reuse the database stuff for this.
    for pattern in ['env_ast_*', 'asteroid_*']:
        # Load the files first.
        Get_All_Indexed_Files('components', pattern)
    # Filter for asteroids.
    asteroid_files = Get_Asset_Files_By_Class('components', 'asteroid')

    # Dict of (game file : xml root), for writeback later.
    game_file_xml_roots = {x: x.Get_Root() for x in asteroid_files}

    # Extract component xml nodes to work with, indexed by name.
    component_nodes = {}
    for xml_root in game_file_xml_roots.values():
        # Loop over the components (probably just one).
        for component in xml_root.xpath('./component'):
            if component.get('class') != 'asteroid':
                continue
            ast_name = component.get('name')
            component_nodes[ast_name] = component

    # Match up asteroids with their materials used, prepping to uniquify.
    # Dict matching material names to asteroids using it.
    # Note: 168 asteroids use 19 materials.
    mat_asteroid_dict = defaultdict(list)

    for component in component_nodes.values():
        # An asteroid may use more than one material.
        mats = []
        for material in component.xpath('.//material'):
            mat_name = material.get('ref')
            # Ignore duplicates (fairly common).
            if mat_name not in mats:
                mats.append(mat_name)

        for mat in mats:
            mat_asteroid_dict[mat].append(component)

    # Handle the specs for when asteroids draw, by size.
    lodvalues_file = Load_File('libraries/region_lodvalues.xml')
    lodvalues_root = lodvalues_file.Get_Root()

    # Change the xxl distance; needs to be <200km (original 250km) for
    # lighting to work. Make even closer, based on zone vis distance.
    # Reduce the component spawning range on xxl asteroids, since it prevents
    # them from displaying when their zone isn't visible.
    # Closest observed pop-in is 42km, so 40km may be mostly safe, but
    # can also drop to 30km like other asteroids.
    # Update: asteroids seen popping in under ~28km.
    # Note: zone visibility range is 25km; can limit to that to be super safe.
    # TODO: maybe bump up zone visibility range to help out (small perf
    # impact).
    # TODO: consider asteroid size, as surface several km from center.
    # Note: 4.0 adds an extra entra for super large objects, comment indicates
    # it is for fog, rendering out to 5000km; leave that alone.
    for distance_node in lodvalues_root.xpath('./distances/distance'):
        # Ignore super big objects.
        if float(distance_node.get('minobjectsize')) > 5000:
            continue
        # Cap to lighting range.
        if float(distance_node.get('render')) > 200000:
            distance_node.set('render', '198000')
            distance_node.set('calculation', '200000')
        # Limit component distance.
        if float(distance_node.get('component')) > 25000:
            distance_node.set('component', '25000')

    # Read out the show distances and size cuttoffs.
    minsize_renderdists = {}
    for distance_node in lodvalues_root.xpath('./distances/distance'):
        min_size = float(distance_node.get('minobjectsize'))
        render_dist = float(distance_node.get('render'))
        minsize_renderdists[min_size] = render_dist

    # In region_lodvalues are also the parameters for the engine-calculated
    # transparency. Terms are for time-based fading when an asteroid spawns,
    # and distance based (based on square of distance according to xsd).
    # A custom distance transparency will be used below and in the shader,
    # so disable it here. Keep the time-based fade.
    # To remove, undefine the value (according to xsd).
    fade_node = lodvalues_root.xpath('./fading')[0]
    # Tinkering with range.
    # Default distance factor is 0.15.
    # Note: even when setting this high, the initial fade-in goes
    # fast enough to feel like a pop; this distance only affects the
    # gradual fading after the initial fast ~30%.
    # -Removed
    #fade_node.set('distance','0.8')
    if disable_ego_fade:
        del fade_node.attrib['distance']

    # Pull up the materials file.
    material_file = Load_File('libraries/material_library.xml')
    material_root = material_file.Get_Root()

    # Gather materials for each asteroid, uniquifying as needed.
    asteroid_materials = defaultdict(list)
    # Names of all materials by collection, used to ensure uniqueness
    # of generated names.
    collection_names = defaultdict(set)

    # Note: loading of the xmf files can be slow if done individually for
    # each asteroid, due to pattern searches.
    # Do a single xmf lod pattern search here, organized by folder name.
    # Try to limit to expected folder names, else this is really slow.
    xmf_folder_files = defaultdict(list)
    for xmf_file in Load_Files('*/asteroids/*lod*.xmf'):
        folder = xmf_file.virtual_path.rsplit('/', 1)[0]
        xmf_folder_files[folder].append(xmf_file)

    for mat_ref, asteroids in mat_asteroid_dict.items():

        # Break the mat_name into a collection and proper name, since
        # the components use <collection>.<mat>
        collection_name, mat_name = mat_ref.split('.', 1)

        # To do binary edits safely, ensure the new name is the same
        # length as the old. Adjusting just the last character isn't quite
        # enough in the worst case that needs >30, so swap the last two
        # characters for a number.
        numbers = '0123456789'
        suffixes = []
        for char0 in numbers:
            for char1 in numbers:
                suffixes.append(char0 + char1)

        # If the collection isn't checked for names yet, check it now.
        if collection_name not in collection_names:
            for mat_node in material_root.xpath(
                    f'./collection[@name="{collection_name}"]/material'):
                collection_names[collection_name].add(mat_node.get('name'))

        material = material_root.find(
            f'./collection[@name="{collection_name}"]/material[@name="{mat_name}"]'
        )
        # Should always be found.
        assert material != None

        # If just one asteroid user, no duplication needed.
        if len(asteroids) == 1:
            asteroid_materials[asteroids[0]].append(material)
            continue

        # Otherwise, make duplicates for each asteroid.
        # (Don't bother reusing the original entry for now.)
        for i, asteroid in enumerate(asteroids):

            mat_copy = deepcopy(material)
            mat_copy.tail = None

            # Give a new, unique name.
            old_name = mat_copy.get('name')
            # Replace the last 2 characters until unique (probably first try
            # will work, except when it is the same as the existing last chars).
            while suffixes:
                char_pair = suffixes.pop(0)
                new_name = old_name[0:-2] + char_pair
                if new_name not in collection_names[collection_name]:
                    collection_names[collection_name].add(new_name)
                    break
            # Don't expect to ever run out of suffixes.
            assert suffixes
            # Ensure same length.
            assert len(old_name) == len(new_name)

            #print(f'copying {old_name} -> {new_name}')
            mat_copy.set('name', new_name)

            # Insert back into the library.
            material.addnext(mat_copy)
            # Screw around with messed up tails.
            if mat_copy.tail != None:
                material.tail = mat_copy.tail
                mat_copy.tail = None

            # Update the asteroid to use this material.
            asteroid_materials[asteroid].append(mat_copy)

            # Updating the xml doesn't actually matter in practice, since the
            # game reads from xmf lod values. But do it anyway.
            old_ref_name = collection_name + '.' + old_name
            new_ref_name = collection_name + '.' + new_name
            for material_ref in asteroid.xpath(
                    f'.//material[@ref="{old_ref_name}"]'):
                material_ref.set('ref', new_ref_name)

            # Look up the xmf lod binaries.
            # These have the old ref name as a string packed with the binary.
            # The data is in a folder, defined in the component.
            # These use backslashes; convert to forward.
            geometry_folder = asteroid.find('./source').get(
                'geometry').replace('\\', '/')

            # Lod files appear to have the name form <prefix>lod<#>.xmf.
            lod_files = xmf_folder_files[geometry_folder]
            assert lod_files

            for lod_file in lod_files:
                print(f'Binary patching {lod_file.virtual_path}')

                # Make a binary edit path, looking to replace the old name
                # with the new name.
                patch = Binary_Patch(
                    file=lod_file.virtual_path,
                    # Convert the strings to hex, each character becoming
                    # two hex digits. These are null terminated in xmf.
                    # (Prevents eg. ast_ore_01 also matching ast_ore_01_frac.)
                    ref_code=String_To_Hex_String(old_ref_name) + '00',
                    new_code=String_To_Hex_String(new_ref_name) + '00',
                    expected_matches=[0, 1],
                )
                # Try the patch; if successful it will tag the file
                # as modified.
                if not Apply_Binary_Patch(patch):
                    # Some mismatch; just breakpoint to look at it.
                    bla = 0

    # Now with all materials being unique, check the asteroid sizes against
    # lodvalues, and update their material fade distances.
    for asteroid, materials in asteroid_materials.items():

        # Determine the asteroid size, based on its max dimension.
        ast_size = 0
        size_max_node = asteroid.find('.//size/max')
        for attr in ['x', 'y', 'z']:
            dim = size_max_node.get(attr)
            assert dim != None
            dim = float(dim)
            if dim > ast_size:
                ast_size = dim

        # Determine the render distance, based on largest matching rule.
        # Loop goes from largest to smallest; break on first match.
        render_dist = None
        for minsize, dist in sorted(minsize_renderdists.items(), reverse=True):
            if ast_size >= minsize:
                render_dist = dist
                break

        # Fade should end at render_dist, start somewhat closer.
        # How much closer is harder to define, but vanilla files are
        # often around 20%, and even a small amount would be enough to
        # offset pop-in.
        # Note: render_dist is for the asteroid center point when it shows
        # up, but camera distance is per-pixel and will be closer, so have
        # fade end a little sooner. Go with 1% for now.
        # Note: dither effect is more noticeable than proper alpha, do try
        # to lowball the fading period, and not use the full 20% except for
        # close stuff.
        fade_end = render_dist * 0.99
        fade_start = render_dist * 0.80
        # Cap the fade range. Assuming travel at 5km/s, 15km would still take
        # 3 seconds to fade in, so might be a decent cap.
        closest_start = fade_end - 15000
        # If making use of egosofts fade, the dither can be done a bit faster,
        # allowing ego fade to take over afterwards.
        #if not disable_ego_fade:
        #    fade_start = render_dist * 0.95
        #    closest_start = fade_end - 5000

        fade_start = max(fade_start, closest_start)

        # Apply these to the material properties.
        for material in materials:
            for type, prop_name, value in [
                ('Float', 'ast_camera_fade_range_start', f'{fade_start:.0f}'),
                ('Float', 'ast_camera_fade_range_stop', f'{fade_end:.0f}'),
            ]:

                # Check if there is already a matching property.
                property = material.find(
                    f'./properties/property[@name="{prop_name}"]')
                # If not found, add it.
                if property == None:
                    property = etree.Element('property',
                                             type=type,
                                             name=prop_name)
                    properties = material.find('./properties')
                    properties.append(property)
                    assert property.tail == None

                # Set or update the value.
                property.set('value', value)

    # Collect from all materials the shaders used, and uniquify their
    # names. Patch_Shader_Files will create the new ones.
    shader_names = []
    for materials in asteroid_materials.values():
        for material in materials:
            mat_shader_name = material.get('shader')
            # Actual file names use ogl instead of fx extension.
            shader_name = mat_shader_name.replace('.fx', '.ogl')
            if shader_name not in shader_names:
                shader_names.append(shader_name)
            material.set('shader', mat_shader_name.replace('.fx', '_faded.fx'))

    # Send them over for updating.
    Patch_Shader_Files(shader_names)

    # Put back the modified materials and asteroids.
    material_file.Update_Root(material_root)
    lodvalues_file.Update_Root(lodvalues_root)
    for game_file, xml_root in game_file_xml_roots.items():
        game_file.Update_Root(xml_root)

    return
예제 #13
0
def Remove_Dock_Symbol():
    '''
    Removes the red dock symbol from small docks.
    '''
    '''
    These are present in the dockingbay_arg_s_01* files.
    These connect to the material:
    <material id="1" ref="p1effects.p1_holograph_dockingbay_noentry"/>
    This happens twice, once in a part named "fx_NE_base" (remove part),
    and once as a part named "fx_NE_dots" (can remove this whole connection,
    since it is a child of fx_NE_base).

    Can either delete the connection (was done originally), or modify
    the material. Go with the latter, as a better catch-all and safe
    against game version changes.

    See remove_dirty_glass for comments on this, but in short, test
    changes with game restarts, and swap bitmaps to be transparent.

    Test result:
    - Only the dots were removed; the solid part of the symbol remains.

    '''
    if 0:
        # Material edits. Failed to fully remove the symbol.
        material_file = Load_File('libraries/material_library.xml')
        xml_root = material_file.Get_Root()

        for mat_name in [
                'p1_holograph_dockingbay_noentry',
        ]:
            mat_node = xml_root.xpath(
                ".//material[@name='{}']".format(mat_name))
            assert len(mat_node) == 1
            mat_node = mat_node[0]

            # Change all bitmaps to use assets\textures\fx\transparent_diff
            for bitmap in mat_node.xpath(
                    "./properties/property[@type='BitMap']"):
                bitmap.set('value', r'assets\textures\fx\transparent_diff')

        material_file.Update_Root(xml_root)

    if 1:
        # Direct connection edits.
        dock_files = Load_Files('*dockingbay_arg_*.xml')

        for game_file in dock_files:
            xml_root = game_file.Get_Root()

            # Remove fx_NE_base part.
            results = xml_root.xpath(".//parts/part[@name='fx_NE_base']")
            if not results:
                continue
            for part in results:
                # Remove it from its parent.
                part.getparent().remove(part)

            # Remove fx_NE_dots parent connection.
            results = xml_root.xpath(
                ".//connection[parts/part/@name='fx_NE_dots']")
            for conn in results:
                # Remove it from its parent.
                conn.getparent().remove(conn)

            # Commit changes right away; don't bother delaying for errors.
            game_file.Update_Root(xml_root)
            # Encourage a better xpath match rule.
            game_file.Add_Forced_Xpath_Attributes(
                "name,parts/part/@name='fx_NE_dots'")

    return
예제 #14
0
def Remove_Debug(empty_diffs = 1):
    '''
    Delete debug_to_text nodes from md and aiscript files.

    * empty_diffs
      - Bool, set True to generate diff files but with no changes, useful
        for reloading a save so that game still sees the diff files.
        
    Result: over 60 second of fps smoothing, after waiting for it to stabalize,
    ignoring first loaded game (that has ~25% higher fps than reloads):
    - without command line logging enabled: no notable difference.
    - with logging enabled: 15.7 to 17.2 fps (+10%).
    - There is reload-to-reload variation, though, so not certain.

    '''
    aiscript_files = Load_Files('aiscripts/*.xml')
    md_files       = Load_Files('md/*.xml')
    
    for game_file in aiscript_files + md_files:
        xml_root = game_file.Get_Root()

        changed = False
        for tag in ['debug_text', 'debug_to_file']:
            debug_nodes = xml_root.xpath(".//{}".format(tag))
            if not debug_nodes:
                continue

            changed = True
            if empty_diffs:
                continue

            for node in debug_nodes:
                # Remove it from its parent.
                node.getparent().remove(node)

        # In some cases a do_if wrapped a debug_text, and can be safely
        # removed as well if it has no other children.
        # The do_if may be followed by do_else that also wrapped debug,
        # so try to work backwards.
        for tag in ['do_else', 'do_elseif', 'do_if']:
            do_nodes = xml_root.xpath(".//{}".format(tag))
            if not do_nodes:
                continue
            
            changed = True
            if empty_diffs:
                continue

            # Loop backwards, so do_elseif pops from back to front.
            # (Not likely to catch anything, but maybe.)
            for node in reversed(do_nodes):
                # Check for children.
                if list(node):
                    continue
                # Check that the next sibling isn't do_else_if or else.
                follower = node.getnext()
                if follower != None and follower.tag in ['do_elseif','do_else']:
                    continue
                # Should be safe to delete.
                node.getparent().remove(node)

        if changed:
            # Commit changes right away; don't bother delaying for errors.
            game_file.Update_Root(xml_root)
    return
예제 #15
0
def Increase_AI_Script_Waits(
    oos_multiplier=2,
    oos_seta_multiplier=4,
    oos_max_wait=15,
    iv_multiplier=1,
    iv_seta_multiplier=1,
    iv_max_wait=5,
    filter='*',
    include_extensions=False,
    skip_combat_scripts=False,
    skip_mining_scripts=True,
):
    '''
    Increases wait times in ai scripts, to reduce their background load
    and improve performance.  Separate modifiers are applied to "in-vision"
    and "out-of-vision" parts of scripts. Expected to have high impact on fps,
    at some cost of ai efficiency.

    * oos_multiplier
      - Float, how much to multiply OOS wait times by. Default is 2.
    * oos_seta_multiplier
      - Float, alternate OOS multiplier to apply if the player
        is in SETA mode. Default is 4.
      - Eg. if multiplier is 2 and seta_multiplier is 4, then waits will
        be 2x longer when not in SETA, 4x longer when in SETA.
    * oos_max_wait
      - Float, optional, the longest OOS wait that this multiplier can achieve,
        in seconds.
      - Defaults to 15.
      - If the original wait is longer than this, it will be unaffected.
    * iv_multiplier
      - As above, but for in-vision.
      - Defaults to 1x, eg. no change.
    * iv_seta_multiplier
      - As above, but for in-vision.
      - Defaults to 1x, eg. no change.
    * iv_max_wait
      - As above, but for in-vision.
      - Defaults to 5.
    * filter
      - String, possibly with wildcards, matching names of ai scripts to
        modify; default is plain '*' to match all aiscripts.
      - Example: "*trade.*" to modify only trade scripts.
    * include_extensions
      - Bool, if True then aiscripts added by extensions are also modified.
      - Defaults False.
    * skip_combat_scripts
      - Bool, if True then scripts which control OOS damage application
        will not be modified. Otherwise, they are modified and their
        attack strength per round is increased to match the longer wait times.
      - Defaults False.
    * skip_mining_scripts
      - Bool, if True then scripts which control OOS mining rates will not
        be modified. Currently has no extra code to adjust mining rates
        when scaled.
      - Defaults True.
      - Note currently expected to signicantly matter with max_wait of 15s,
        since OOS mining waits at least 16s between gathers.
    '''

    # Just ai scripts; md has no load.
    aiscript_files = Load_Files(
        f"{'*' if include_extensions else ''}aiscripts/{filter}.xml")

    # Combine oos/iv stuff into a dict for convenience.
    vis_params = {
        'iv': {
            'multiplier': iv_multiplier,
            'seta_multiplier': iv_seta_multiplier,
            'max_wait': iv_max_wait,
        },
        'oos': {
            'multiplier': oos_multiplier,
            'seta_multiplier': oos_seta_multiplier,
            'max_wait': oos_max_wait,
        },
    }

    # Set up the string with the multiplication.
    for entry in vis_params.values():
        entry[
            'mult_str'] = "(if player.timewarp.active then {} else {})".format(
                entry['seta_multiplier'], entry['multiplier'])

    for game_file in aiscript_files:
        xml_root = game_file.Get_Root()
        file_name = game_file.name.replace('.xml', '')

        # Find any get_attack_strength nodes, used in OOS combat.
        attack_strength_nodes = xml_root.xpath(".//get_attackstrength")
        # If there are any, and not modifying combat scripts, skip.
        if attack_strength_nodes and skip_combat_scripts:
            continue

        # Find any get_resource_gatherrate nodes, used in OOS mining.
        gatherrate_nodes = xml_root.xpath(".//get_resource_gatherrate")
        # If there are any, and not modifying mining scripts, skip.
        if gatherrate_nodes and skip_mining_scripts:
            continue

        # Find all waits.
        nodes = xml_root.xpath(".//wait")
        if not nodes:
            continue

        # Find any waits under visible attention as well.
        visible_waits = xml_root.xpath('.//attention[@min="visible"]//wait')

        # Loop over iv, oos.
        for mode, params in vis_params.items():
            # Unpack for convenience.
            multiplier = params['multiplier']
            seta_multiplier = params['seta_multiplier']
            max_wait = params['max_wait']
            mult_str = params['mult_str']

            # If the multipliers are just 1x or None, skip.
            if multiplier in [1, None] and seta_multiplier in [1, None]:
                continue

            for node in nodes:
                # Skip if visible in oos, or if not visible in iv.
                if mode == 'oos' and node in visible_waits:
                    continue
                if mode == 'iv' and node not in visible_waits:
                    continue

                # TODO: figure out a good way to record the wait length
                # for pre-atack_strength waits, to use in adjusting the
                # applied damage more precisely (eg. avoid mis-estimate
                # when play goes in/out of seta during the pre-attack wait.

                for attr in ['min', 'max', 'exact']:
                    orig = node.get(attr)
                    if not orig:
                        continue

                    # This will get the orig value, the orig value multiplied
                    # with a ceiling, and take the max.
                    new = f'[{orig}, [{max_wait}s, ({orig})*{mult_str}].min].max'
                    node.set(attr, new)

            # If this is in-vision mode, skip the oos attack_strength stuff.
            if mode == 'iv':
                continue

            # Adjust attack strength to account for the timing change.
            for node in attack_strength_nodes:
                # For now, assume the waits were fully multiplied, and didn't
                # cap out at max_wait. Combat scripts appear to use a 1s
                # period (since attack_strength is dps), so this shouldn't
                # be an issue.

                # This can have up to 5 output values, that all need the
                # same multiplication.
                # A unified result is in a 'result' attribute.
                # Specific damages (shield, hull, etc.) are in a 'result'
                # subnode.
                # Gather the names of capture vars.
                out_vars = []

                if node.get('result'):
                    out_vars.append(node.get('result'))
                result_subnode = node.find('./result')

                if result_subnode != None:
                    for attr in [
                            'hullshield', 'hullonly', 'shieldonly',
                            'hullnoshield'
                    ]:
                        varname = result_subnode.get(attr)
                        if varname:
                            out_vars.append(varname)

                # Add in set_value nodes to multiply each of these.
                for varname in out_vars:
                    new_node = Element('set_value',
                                       name=varname,
                                       exact=f'{varname} * {mult_str}')
                    node.addnext(new_node)
                    # lxml can mess up node id tails, so fix it.
                    if new_node.tail != None:
                        assert node.tail == None
                        node.tail = new_node.tail
                        new_node.tail = None

            game_file.Update_Root(xml_root)

    return
'''
Load files using the file system, with automated patch application from
enabled extensions, then save the patched xml for easier viewing.
'''
from Plugins import *
from Framework import Load_Files

# Pick a location to save to.
# This will go to X4root/extracted_patched
folder = Settings.Get_X4_Folder() / 'extracted_patched'

game_files = []

# Pick a set of file name patterns.
# This example includes all library xml files.
for pattern in [
    'libraries/*.xml',
    ]:
    game_files += Load_Files(pattern)

# This code pulls the file binary data (not diff encoded), and writes to
# the target directory.
for game_file in game_files:
    binary = game_file.Get_Binary(no_diff = True)
    file_path = folder / game_file.virtual_path
    if not file_path.parent.exists():
        file_path.parent.mkdir(parents = True)
    with open(file_path, 'wb') as file:
        file.write(binary)

예제 #17
0
def Scale_Sector_Size(
        scaling_factor,
        scaling_factor_2=None,
        transition_size_start=200000,
        transition_size_end=400000,
        # TODO: make a decision on if this is good or not.
        # Maybe helps with hatikvahs highway/asteroid overlap?
        recenter_sectors=False,
        precision_steps=10,
        remove_ring_highways=False,
        remove_nonring_highways=False,
        extra_scaling_for_removed_highways=0.7,
        scale_regions=True,
        move_free_ships=True,
        debug=True,
        _test=False):
    '''
    Change the size of the maps by moving contents (zones, etc.) closer
    together or further apart. Note: this will require a new game to
    take effect, as positions become part of a save file.

    * scaling_factor
      - Float, how much to adjust distances by.
      - Eg. 0.5 to cut sector size roughly in half.
    * scaling_factor_2
      - Float, optional, secondary scaling factor to apply to large sectors.
      - If not given, scaling_factor is used for all sectors.
    * transition_size_start
      - Int, sector size at which to start transitioning from
        scaling_factor to scaling_factor_2.
      - Defaults to 200000.
      - Sectors smaller than this will use scaling_factor.
    * transition_size_end
      - Int, optional, sector size at which to finish transitioning to
        scaling_factor_2.
      - Defaults to 400000 (400 km).
      - Sectors larger than this will use scaling_factor_2.
      - Sectors of intermediate size have their scaling factor interpolated.
    * recenter_sectors
      - Adjust objects in a sector to approximately place the coreposition
        near 0,0,0.
      - Defaults False.
      - In testing, this makes debugging harder, and may lead to unwanted
        results.  Pending further testing to improve confidence.
    * num_steps
      - Int, over how many movement steps to perform the scaling.
      - Higher step counts take longer to process, but each movement is
        smaller and will better detect objects getting too close to each other.
      - Recommend lower step counts when testing, high step count for
        a final map.
      - Defaults to 10.
    * remove_ring_highways
      - Bool, set True to remove the ring highways.
    * remove_nonring_highways
      - Bool, set True to remove non-ring highways.
    * extra_scaling_for_removed_highways
      - Float, extra scaling factor to apply to sectors that had highways
        removed.
      - Defaults to 0.7.
    * scale_regions
      - Bool, if resource and debris regions should be scaled as well.
      - May be slightly off from sector scalings, since many regions are
        shared between sectors.
      - Defaults True.
    * move_free_ships
      - Bool, if ownerless ships spawned at game start should be moved
        along with the other sector contents.
      - May impact difficulty of finding these ships.
      - Defaults True.
    * debug
      - Bool, if True then write runtime state to the plugin log.
    '''

    # Use a pattern to pick up the base and dlc sectors.
    # Store the game_file as key, xml root as value.
    # Below transforms exit the xml root, which gets committed at the end
    # if there were no errors.
    # Note: for quick testing, just grab the basic files, no wildcards.
    if _test:
        gamefile_roots = {
            'sectors':
            [(x, x.Get_Root())
             for x in [Load_File('maps/xu_ep2_universe/sectors.xml')]],
            'zones': [(x, x.Get_Root())
                      for x in [Load_File('maps/xu_ep2_universe/zones.xml')]],
            'zone_highways':
            [(x, x.Get_Root())
             for x in [Load_File('maps/xu_ep2_universe/zonehighways.xml')]],
            'clusters':
            [(x, x.Get_Root())
             for x in [Load_File('maps/xu_ep2_universe/clusters.xml')]],
            'sec_highways':
            [(x, x.Get_Root())
             for x in [Load_File('maps/xu_ep2_universe/sechighways.xml')]],
        }
    else:
        gamefile_roots = {
            'sectors':
            [(x, x.Get_Root())
             for x in Load_Files('*maps/xu_ep2_universe/*sectors.xml')],
            'zones': [(x, x.Get_Root())
                      for x in Load_Files('*maps/xu_ep2_universe/*zones.xml')],
            'zone_highways':
            [(x, x.Get_Root())
             for x in Load_Files('*maps/xu_ep2_universe/*zonehighways.xml')],
            'clusters':
            [(x, x.Get_Root())
             for x in Load_Files('*maps/xu_ep2_universe/*clusters.xml')],
            'sec_highways':
            [(x, x.Get_Root())
             for x in Load_Files('*maps/xu_ep2_universe/*sechighways.xml')],
        }
    gamefile_roots.update({
        'region_defs':
        [(x, x.Get_Root())
         for x in [Load_File('libraries/region_definitions.xml')]],
        'md_hq': [(x, x.Get_Root())
                  for x in [Load_File('md/X4Ep1_Mentor_Subscription.xml')]],
        'md_stations': [(x, x.Get_Root())
                        for x in [Load_File('md/FactionLogic_Stations.xml')]],
        'md_objects': [(x, x.Get_Root())
                       for x in [Load_File('md/PlacedObjects.xml')]],
        'md_gs_split1':
        [(x, x.Get_Root())
         for x in [Load_File('extensions/ego_dlc_split/md/gs_split1.xml')]],
        'god': [(x, x.Get_Root()) for x in [Load_File('libraries/god.xml')]],
        'gamestarts': [(x, x.Get_Root())
                       for x in [Load_File('libraries/gamestarts.xml')]],
    })

    def Safe_Update_MD(xml_root, xpath, attr, old_text, new_text):
        'Helper function for editing md nodes.'
        # Note: add some safety incase the lookups fail.
        nodes = xml_root.xpath(xpath)
        if not nodes:
            msg = 'Scale_Sector_Size failed to find a target MD script node; skipping this node.'
            Plugin_Log.Print(msg)
            Print(msg)
        else:
            nodes[0].set(attr, nodes[0].get(attr).replace(old_text, new_text))

    # Tweak faction logic to spawn stations closer/further.
    faction_stations_file = Load_File('md/FactionLogic_Stations.xml')
    faction_stations_root = faction_stations_file.Get_Root()

    # TODO: what is the difference between sector.size and sector.coresize?
    Safe_Update_MD(
        faction_stations_root,
        ".//match_distance[@max='[$ChosenSector.coresize / 2.0f, 400km].min']",
        'max', '400km', str(int(400 * scaling_factor)))

    Safe_Update_MD(
        faction_stations_root,
        ".//set_value[@exact='[$ChosenSector.size / 2.0f, 400km].min']",
        'exact', '400km', str(int(400 * scaling_factor)))

    # FactionLogic.xml:
    # (Used in selecting existing sector; coreposition usage okay.)
    # <match_distance space="$Sector" value="$Sector.coreposition" max="[$Sector.coresize, 400km].min"/>
    faction_logic_file = Load_File('md/FactionLogic.xml')
    faction_logic_root = faction_logic_file.Get_Root()

    Safe_Update_MD(faction_logic_root,
                   ".//match_distance[@max='[$Sector.coresize, 400km].min']",
                   'max', '400km', str(int(400 * scaling_factor)))

    # Adjust the deepspace search lib to have a shorter distance.
    lib_deepspace_file = Load_File('aiscripts/lib.find.point.indeepspace.xml')
    lib_deepspace_root = lib_deepspace_file.Get_Root()
    # The logic here checks for the furthest station position, then adds
    # a random 50-150km to it when picking a point.
    # All calls to this use the default params (50,150).
    # Can reduce the defaults.
    for name in ['mindistance', 'maxdistance']:
        param_node = lib_deepspace_root.find(f'./params/param[@name="{name}"]')
        # These should be 50km and 150km, but dynamically scale to be safe.
        value = param_node.get('default')
        if 'km' in value:
            value = float(value.replace('km', '')) * 1000
        else:
            value = float(value)
        new_value = value * scaling_factor
        param_node.set('default', f'{new_value:.0f}')

    # Build station missions can select points pretty far out. Scale.
    # Of interest are a couple attributes of 300km.
    # (Be lazy here and assume unchanged.)
    gm_build_stations_file = Load_File('md/gm_buildstation.xml')
    gm_build_stations_root = gm_build_stations_file.Get_Root()
    for node in gm_build_stations_root.xpath(
            '//create_position[@max="300km"]'):
        node.set('max', '{:.0f}'.format(300000 * scaling_factor))

    # The defeated split start has a tight oxygen timer (3.5 minutes);
    # boost it up for more slack in the rescaled time for the rescue
    # ship to arrive.
    md_gs_split1_root = gamefile_roots['md_gs_split1'][0][1]
    oxygen_node = md_gs_split1_root.find('.//set_spacesuit_oxygen')
    # Was 12%. Bump to 30 (~10 minutes).
    oxygen_node.set('percent', '30')

    # Load in data of interest, to the local data structure.
    galaxy = Galaxy(gamefile_roots,
                    recenter_sectors=recenter_sectors,
                    move_free_ships=move_free_ships)

    # TODO: record starting sector size for debug.

    # Set scaling factor per sector based on size.
    sector_scaling_factors = {}
    for sector in galaxy.class_macros['sectors'].values():
        # Get raw size, with no minimum.
        size = sector.Get_Size(apply_minimum=False)
        if scaling_factor_2 == None or size < transition_size_start:
            sector_scaling_factors[sector] = scaling_factor
        elif size > transition_size_end:
            sector_scaling_factors[sector] = scaling_factor_2
        else:
            # Linear interpolate.
            ratio = (size - transition_size_start) / (transition_size_end -
                                                      transition_size_start)
            scaling = scaling_factor_2 * ratio + scaling_factor * (1 - ratio)
            sector_scaling_factors[sector] = scaling

    # Handle highway removal.
    for sector in galaxy.class_macros['sectors'].values():
        # Two types of removal, but one flag to indicate removal happened.
        highways_removed = False

        if remove_ring_highways and sector.Has_Ring_Highway():
            sector.Remove_Ring_Highways()
            highways_removed = True

        if remove_nonring_highways and sector.Has_Nonring_Highway():
            sector.Remove_Nonring_Highways()
            highways_removed = True

        # Adjust the scaling.
        if highways_removed:
            sector_scaling_factors[
                sector] *= extra_scaling_for_removed_highways

    # Run the repositioning routines.
    # TODO: region scaling factor?
    if scale_regions:
        Scale_Regions(galaxy, sector_scaling_factors, debug)
    Scale_Sectors(galaxy,
                  sector_scaling_factors,
                  debug,
                  precision_steps=precision_steps)

    # Update the xml nodes.
    galaxy.Update_XML()

    # TODO: print sector size change summary.

    # If here, everything worked, so commit the updates.
    for file_roots in gamefile_roots.values():
        # TODO: maybe skip clusters, as unmodified.
        for game_file, new_root in file_roots:
            game_file.Update_Root(new_root)

    faction_stations_file.Update_Root(faction_stations_root)
    faction_logic_file.Update_Root(faction_logic_root)
    lib_deepspace_file.Update_Root(lib_deepspace_root)
    gm_build_stations_file.Update_Root(gm_build_stations_root)

    return
예제 #18
0
def Annotate_Script_Names(empty_diffs = 0):
    '''
    For every ai script, annotate the pilot entity with the name
    of the script running.
    '''
    aiscript_files = Load_Files('aiscripts/*.xml')
    
    for game_file in aiscript_files:
        xml_root = game_file.Get_Root()

        # Do this on every blocking action command.
        changed = False
        for tag in [
            'dock_masstraffic_drone',
            'execute_custom_trade',
            'execute_trade',
            'move_approach_path',
            'move_docking',
            'move_undocking',
            'move_gate',
            'move_navmesh',
            'move_strafe',
            'move_target_points',
            'move_waypoints',
            'move_to',
            'detach_from_masstraffic',
            'wait_for_prev_script',
            'wait',
            ]:
            nodes = xml_root.xpath(".//{}".format(tag))
            if not nodes:
                continue

            changed = True
            if empty_diffs:
                continue

            for node in nodes:

                # Before the wait, write to the pilot the file name.
                script_name = etree.Element('set_value', 
                    name = 'this.$script_name', 
                    # File name, in extra quotes.
                    exact = "'{}'".format(game_file.name.replace('.xml','')))
                node.addprevious(script_name)

                # Blocking action may be of interest.
                element_name = etree.Element('set_value', 
                    name = 'this.$element_name',
                    exact = "'{}'".format(tag))
                node.addprevious(element_name)

                # Version of script name with the line number.
                # Can only do this for blocking elements that have a line.
                if node.sourceline:
                    name_line = "'${} {}'".format(game_file.name.replace('.xml',''), node.sourceline)

                    script_line_node = etree.Element('set_value', 
                        name = 'this.$script_line_name', 
                        # File name, in extra quotes, followed by line.
                        exact = name_line)
                    node.addprevious(script_line_node)

                    # More complicated: set the pilot to count how many
                    # times each script/line block was reached.
                    # This is likely to better represent hot code that just
                    # the number of ai ships sitting on some blocking action.
                    record_group = [
                        etree.fromstring('''
                            <do_if value="not this.$script_line_counts?">
                              <set_value name="this.$script_line_counts" exact="table[]"/>
                            </do_if>'''),
                        # Can accumulate a long time, so use a float or long int.
                        etree.fromstring('''
                            <do_if value="not this.$script_line_counts.{FIELD}?">
                              <set_value name="this.$script_line_counts.{FIELD}" exact="0.0"/>
                            </do_if>'''.replace('FIELD', name_line)),
                        etree.fromstring(('''
                            <set_value name="this.$script_line_counts.{FIELD}" operation="add"/>'''
                            ).replace('FIELD', name_line)),
                    ]
                    for record_node in record_group:
                        node.addprevious(record_node)


        if changed:
            # Commit changes right away; don't bother delaying for errors.
            game_file.Update_Root(xml_root)
    return
예제 #19
0
def Remove_Dock_Glow():
    '''
    Removes the glow effect from station docks.
    '''

    '''
    Of interest are the connections that define the parts for the fx glow/haze.
    Examples:
        ' <connection name="Connection04" tags="part detail_xl nocollision fx  ">
        '   ...
        '   <parts>
        '     <part name="fx_haze">
        '   ...
    and
        ' <connection name="Connection02" tags="part detail_l nocollision fx  ">
        '   ...
        '   <parts>
        '     <part name="fx_glow">
        '       <lods>
        '         <lod index="0">
        '           <materials>
        '             <material id="1" ref="p1effects.p1_lightcone_soft"/>
        '           </materials>
        '         </lod>
        '       </lods>
        '       <size>
        '         <max x="719.8841" y="717.2308" z="662.7744"/>
        '         <center x="-3.051758E-05" y="349.1792" z="13.29515"/>
        '       </size>
        '     </part>
        '   </parts>
        '   ...

    In testing:
        Glow is the giant blue ball effect.
        Haze is a greyish fog close to the platform.
    Remove just glow.

    Note: terran stations use "dockarea_ter_m...", but do not have glow
    effect, so can ignore them here.

    TODO: look instead for p1effects.p1_lightcone_soft, which shows up in some
    other places. Notably, some landmarks and piers. Maybe check the size
    being above some threshold.
    '''
    # Find every "dockarea" file.
    dock_files = Load_Files('*dockarea_arg_m_station*.xml')

    for game_file in dock_files:
        xml_root = game_file.Get_Root()

        results = xml_root.xpath(".//connection[parts/part/@name='fx_glow']")
        if not results:
            continue

        for conn in results:
            # Remove it from its parent.
            conn.getparent().remove(conn)        

        # Commit changes right away; don't bother delaying for errors.
        game_file.Update_Root(xml_root)
        # Encourage a better xpath match rule.
        game_file.Add_Forced_Xpath_Attributes("parts/part/@name='fx_glow'")
    return