コード例 #1
0
def Tweak_Escorts(empty_diffs = 0):
    '''
    Adjust escort scripts to fire less often.
    Test result: negligible difference (5% at most, probably noise).
    '''
    game_file = Load_File('aiscripts/order.fight.escort.xml')
    xml_root = game_file.Get_Root()
    '''
    These scripts run far more often than anything else, specifically
    hitting a 500 ms wait then doing a move_to_position, even when
    out of visibility.
    Can try increasing the wait to eg. 5s.

    Results:
    - New game, 0.4x sector size, 3x job ships, flying 300 km above trinity
      sanctum in warrior start ship, pointing toward the sector center.
    - Driver set to adaptive power, adaptive vsync (60 fps cap).
    - Game restart between tests, other major programs shut down (no firefox).
    - With change: 37.3 fps, escort wait+move at 20% of aiscript hits.
    - Without change: 41.3 fps, escort wait_move at 40% of aiscript hits.
    - Retest change: 42.8 fps
    - Retest no change: 40.8 fps
    - Retest change: 43.1
    Around a 2 fps boost, or 5%.
    '''
    if not empty_diffs:
        wait_node = xml_root.find('./attention[@min="unknown"]/actions/wait[@exact="500ms"]')
        wait_node.set('exact', '5s')
    game_file.Update_Root(xml_root)
    return
コード例 #2
0
ファイル: Wares.py プロジェクト: bvbohnen/X4_Customizer
def Adjust_Ware_Price_Spread(
        # Allow multipliers to be given as a loose list of args.
        *match_rule_multipliers
    ):
    '''
    Adjusts ware min to max price spreads. This primarily impacts
    trading profit. Spread will be limited to ensure 10 credits
    from min to max, to avoid impairing AI decision making.

    * match_rule_multipliers:
      - Series of matching rules paired with the spread multipliers to use.
    '''
    wares_file = Load_File('libraries/wares.xml')
    xml_root = wares_file.Get_Root()

    # Get wars paired with multipliers.
    for ware, multiplier in Gen_Wares_Matched_To_Args(xml_root, match_rule_multipliers):
        
        # Look up the existing spread.
        price_node = ware.find('./price')
        price_min  = int(price_node.get('min'))
        price_avg  = int(price_node.get('average'))
        price_max  = int(price_node.get('max'))

        # If price is 0 or 1, just skip.
        if price_avg in [0,1]:
            continue

        # Can individually adjust the min and max separations from average.
        new_min = round(price_avg - (price_avg - price_min) * multiplier)
        new_max = round(price_avg + (price_max - price_avg) * multiplier)

        # Limit to a spread of 10 credits or more from min to max,
        # or 5 from average.
        if new_min > price_avg - 5:
            new_min = price_avg - 5
        if new_max < price_avg + 5:
            new_max = price_avg + 5

        # If min dropped to 0, bump it back to 1.
        if new_min <= 0:
            new_min = 1
            # Adjust max to have the same spread from average.
            new_max = price_avg + (price_avg - new_min)

        # Put them back.
        price_node.set('min', str(int(new_min)))
        price_node.set('max', str(int(new_max)))

    wares_file.Update_Root(xml_root)
    return
コード例 #3
0
def Remove_Highway_Blobs():
    highway_file = Load_File('libraries/highwayconfigurations.xml')
    xml_root = highway_file.Get_Root()

    '''
    Set the config for superhighways:
      <blockerconfiguration ref="super_hw_blocker_config" />
    to
      <blockerconfiguration ref="empty_blocker_config" />

    TODO: maybe play around with other highway properties, eg. the
    blur effect, any ad signs (are they here?), mass traffic.
    '''
    superhighway_node = xml_root.find('./configurations/configuration[@id="defaultsuperhighwayconfiguration"]')
    blocker_node = superhighway_node.find('./blockerconfiguration')
    blocker_node.set('ref', 'empty_blocker_config')
    highway_file.Update_Root(xml_root)
コード例 #4
0
ファイル: Wares.py プロジェクト: bvbohnen/X4_Customizer
def Adjust_Ware_Prices(
        # Allow multipliers to be given as a loose list of args.
        *match_rule_multipliers
    ):
    '''
    Adjusts ware prices. This should be used with care when selecting
    production chain related wares.
    
    * match_rule_multipliers:
      - Series of matching rules paired with the spread multipliers to use.
    '''
    wares_file = Load_File('libraries/wares.xml')
    xml_root = wares_file.Get_Root()

    # Get wars paired with multipliers.
    for ware, multiplier in Gen_Wares_Matched_To_Args(xml_root, match_rule_multipliers):
        
        # Adjust everything in the price subnode.
        price = ware.find('price')
        for name, value in price.items():
            XML_Multiply_Int_Attribute(price, name, multiplier)
            
    wares_file.Update_Root(xml_root)
    return
コード例 #5
0
def Decrease_Radar(empty_diffs = 0):
    '''
    Reduce radar ranges, to reduce some compute load for npc ships.
    '''
    '''
    Test on 20k ships save above trinity sanctum.
    5/4    : 35 fps   (50km)
    1/1    : 37 fps   (40km, vanilla, fresh retest)
    3/4    : 39 fps   (30km, same as x3 triplex)
    1/2    : 42 fps   (20km, same as x3 duplex)
    1/4    : 46 fps   (10km, probably too short)

    TODO: small ships 20k, large ships 30k, very large 40k?
    '''
    game_file = Load_File('libraries/defaults.xml')
    xml_root = game_file.Get_Root()

    for node in xml_root.xpath('.//radar'):
        range = node.get('range')
        if range:
            range = float(range)
            node.set('range', str(range * 2/4))
    game_file.Update_Root(xml_root)
    return
コード例 #6
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
コード例 #7
0
ファイル: Jobs.py プロジェクト: bvbohnen/X4_Customizer
def Adjust_Job_Count(
    # Allow job multipliers to be given as a loose list of args.
    *job_multipliers, ):
    '''
    Adjusts job ship counts using a multiplier, affecting all quota fields.
    Input is a list of matching rules, determining which jobs get adjusted.

    Resulting non-integer job counts are rounded, with a minimum of 1 unless
    the multiplier or original count were 0.

    * job_multipliers
      - Tuples holding the matching rules and job count multipliers,
        ("key  value", multiplier).
      - The "key" specifies the job field to look up, which will
        be checked for a match with "value".
      - If a job matches multiple rules, the first match is used.
      - Subordinates will never be matched except by an exact 'id' match, to
        avoid accidental job multiplication (eg. XL ship with a wing of L
        ships which have wings of M ships which have wings of S ships).
      - Supported keys:
        - 'id'      : Name of the job entry; supports wildcards for non-wings.
        - 'faction' : The name of the faction.
        - 'tags'    : One or more tags, space separated.
        - 'size'    : The ship size suffix, 'xs','s','m','l', or 'xl'.
        - '*'       : Matches all non-wing jobs; takes no value term.

    Examples:
    <code>
        Adjust_Job_Count(1.2)
        Adjust_Job_Count(
            ('id       masstraffic*'      , 0.5),
            ('tags     military destroyer', 2  ),
            ('tags     miner'             , 1.5),
            ('size     s'                 , 1.5),
            ('faction  argon'             , 1.2),
            ('*'                          , 1.1) )
    </code>
    '''
    # Put matching rules in standard form.
    rules = Standardize_Match_Rules(job_multipliers)

    jobs_game_file = Load_File('libraries/jobs.xml')
    xml_root = jobs_game_file.Get_Root()

    # Loop over the jobs.
    for job in xml_root.findall('./job'):

        quota = job.find('quota')
        # Check if this is a wing ship.
        # Could also check modifiers.subordinate.
        is_wing = quota.get('wing') != None

        # Look up the tags and a couple other properties of interest.
        job_id = job.get('id')
        # The category node may not be present.
        category = job.find('category')
        if category != None:
            faction = category.get('faction')
            size = category.get('size')
            # Parse the tags to separate them, removing
            #  brackets and commas splitting.
            tags = [
                x.strip(' []') for x in category.get('tags').split(',') if x
            ]
        else:
            faction = None
            size = None
            tags = []

        # Always ignore the dummy_job, to be safe.
        if job_id == 'dummy_job':
            continue

        # Check the matching rules.
        multiplier = None
        for key, value, mult in rules:

            # Non-wing matches.
            if not is_wing:
                if ((key == '*') or (key == 'id' and fnmatch(job_id, value))
                        or (key == 'faction' and faction == value)
                        # Check all tags, space separated.
                        or (key == 'tags' and all(x in tags
                                                  for x in value.split(' ')))
                        # For sizes, add a 'ship_' prefix to the match_str.
                        or (key == 'size' and size == ('ship_' + value))):
                    multiplier = mult
                    break

            # Restrictive wing matches.
            else:
                # Only support on a perfect id match.
                if key == 'id' and job_id == value:
                    multiplier = mult
                    break

        # Skip if no match.
        if multiplier == None:
            continue

        # Apply the multiplier to fields of the quota node.
        for name, value in quota.items():
            XML_Multiply_Int_Attribute(quota, name, multiplier)

    jobs_game_file.Update_Root(xml_root)
    return
コード例 #8
0
def Adjust_Mission_Rewards(
    # Allow multipliers to be given as a loose list of args.
    multiplier=1,
    adjust_credits=True,
    adjust_notoriety=True,
):
    '''
    Adjusts generic mission credit and notoriety rewards by a flat multiplier.

    * multiplier
      - Float, value to adjust rewards by.
    * adjust_credits
      - Bool, if True (default) changes the credit reward.
    * adjust_notoriety
      - Bool, if True (default) changes the notoriety reward.
    '''
    '''
    The generic missions make use of LIB_Reward_Balancing, giving
    it a couple mission aspects (difficulty, complexity level)
    to get the reward credits and notoriety.

    To globally adjust payouts, just edit that LIB script.

    For credits:
        There are a few places to make the edit, but want to do it
        before the value gets rounded to the nearest 10.

        Can either edit an expression in an existing node, eg. the
        faction adjustment, or can insert a new instruction.
        Easiest is probably just to edit the rounding operation directly,
        since that is likely to be stable across version changes.

    For notoriety:
        There is no convenient rounding node, so edit the scaling
        node. That is more likely to change in patches, so do it
        carefully.
    '''
    lib_reward_file = Load_File('md/LIB_Reward_Balancing.xml')
    xml_root = lib_reward_file.Get_Root()

    if adjust_credits:
        # Find the credit rounding instruction.
        credit_node = xml_root.findall(
            './/cue[@name="Reward_Money"]//set_value[@name="$Value"][@exact="(($Value)i / 10) * 10"]'
        )
        # Ensure 1 match.
        assert len(credit_node) == 1
        credit_node = credit_node[0]

        # Edit the operation to include an extra multiply.
        mult_str = Float_to_String(multiplier)
        credit_node.set('exact', '(($Value * {})i / 10) * 10'.format(mult_str))

    if adjust_notoriety:
        # Find the notoriety scaling instruction.
        # Don't do a strict match on the operation; leave it flexible.
        notoriety_node = xml_root.findall(
            './/cue[@name="Reward_Notoriety"]//set_value[@name="$Value"]')
        # Ensure 1 match.
        assert len(notoriety_node) == 1
        notoriety_node = notoriety_node[0]

        # Edit the min and max attributes to include an extra multiply.
        for attrib in ['min', 'max']:
            op = notoriety_node.get(attrib)
            op = op.replace('$Value', '($Value * {})'.format(mult_str))
            notoriety_node.set(attrib, op)

    lib_reward_file.Update_Root(xml_root)
    return
コード例 #9
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
コード例 #10
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
コード例 #11
0
def Update_Content_XML_Dependencies():
    '''
    Update the dependencies in the content xml file, based on which other
    extensions touched files modified by the current script.
    If applied to an existing content.xml (not one created here), existing
    dependencies are kept, and only customizer dependencies are updated.

    Note: an existing xml file may loose custom formatting.
    '''
    # TODO: framework needs more development to handle cases with an
    # existing content.xml cleanly, since currently the output extension is
    # always ignored, and there is no particular method of dealing with
    # output-ext new files not having an extensions/... path.

    # Try to load a locally created content.xml.
    content_file = Load_File('content.xml', error_if_not_found = False)

    # If not found, then search for an existing content.xml on disk.
    if not content_file:
        # Manually load it.
        content_path = Settings.Get_Output_Folder() / 'content.xml'
        # Verify the file exists.
        if not content_path.exists():
            Print('Error in Update_Content_XML_Dependencies: could not find an existing content.xml file')
            return

        content_file = File_System.Add_File( XML_File(
            # Plain file name as path, since this will write back to the
            # extension folder.
            virtual_path = 'content.xml',
            binary = content_path.read_bytes(),
            # Edit the existing file.
            edit_in_place = True,
            ))

    root = content_file.Get_Root()
    
    # Set the ID based on replacing spaces.
    this_id = Settings.extension_name.replace(' ','_')
    
    # Remove old dependencies from the customizer, and record others.
    existing_deps = []
    for dep in root.xpath('./dependency'):
        if dep.get('from_customizer'):
            dep.getparent().remove(dep)
        else:
            existing_deps.append(dep.get('id'))

    # Add in dependencies to existing extensions.
    # These should be limited to only those extensions which sourced
    #  any of the files which were modified.
    # TODO: future work can track specific node edits, and set dependencies
    #  only where transform modified nodes might overlap with extension
    #  modified nodes.
    # Dependencies use extension ids, so this will do name to id
    #  translation.
    # Note: multiple dependencies may share the same ID if those extensions
    #  have conflicting ids; don't worry about that here.
    source_extension_ids = set()
    for file_name, game_file in File_System.game_file_dict.items():
        if not game_file.modified:
            continue
        
        for ext_name in game_file.source_extension_names:
            # Translate extension names to ids.
            ext_id = File_System.source_reader.extension_source_readers[
                                ext_name].extension_summary.ext_id
            source_extension_ids.add(ext_id)

    # Add the elements; keep alphabetical for easy reading.
    for ext_id in sorted(source_extension_ids):

        # Omit self, just in case; shouldn't come up, but might.
        if ext_id == this_id:
            Print('Error: output extension appears in its own dependencies,'
                  ' indicating it transformed its own prior output.')
            continue

        # Skip if already defined.
        if ext_id in existing_deps:
            continue
        
        # Add it, with a tag to indicate it came from the customizer.
        # TODO: optional dependencies?
        root.append( ET.Element('dependency', id = ext_id, from_customizer = 'true' ))

    content_file.Update_Root(root)
    return
コード例 #12
0
ファイル: Adjust.py プロジェクト: AxleUnix/X4_Customizer
def Set_Default_Radar_Ranges(**class_distances):
    '''
    Sets default radar ranges.  Granularity is station, type of satellite,
    or per ship size.  Ranges are in km, eg. 40 for vanilla game
    defaults of non-satellites. Note: ranges below 40km will affect
    when an unidentified object becomes identified, but objects
    will still show up out to 40km.
        
    Supported arguments:
    * ship_s
    * ship_m
    * ship_l
    * ship_xl
    * spacesuit
    * station
    * satellite
    * adv_satellite
    '''
    '''
    Test on 20k ships save above trinity sanctum, multiplying radar ranges:
    5/4    : 35 fps   (50km)
    1/1    : 37 fps   (40km, vanilla, fresh retest)
    3/4    : 39 fps   (30km, same as x3 triplex)
    1/2    : 42 fps   (20km, same as x3 duplex)
    1/4    : 46 fps   (10km, probably too short)

    Satellites:
        eq_arg_satellite_01_macro - 30k
        eq_arg_satellite_02_macro - 75k

    Ships:
        Defined in libraries/defaults.xml per ship class.
        Note: there is a basic radar node, and a max/radar node, which
        presumably is related to ship mods.
        Radars are normally 40k, maxing at 48k.
    '''
    # Start with defaults.
    defaults_file = Load_File('libraries/defaults.xml')
    defaults_root = defaults_file.Get_Root()

    # Scale ranges to meters.
    for class_name, new_range in class_distances.items():
        class_distances[class_name] = new_range * 1000

    # Look for each specified class. This will skip satellites.
    for class_name, new_range in class_distances.items():
        dataset = defaults_root.find(f"./dataset[@class='{class_name}']")
        if dataset == None:
            continue

        # Update range directly.
        radar_node = dataset.find('./properties/radar')
        if radar_node == None:
            continue
        radar_node.set('range', str(new_range))

        # Check if there is a max range.
        max_radar_node = dataset.find('./properties/statistics/max/radar')
        if max_radar_node == None:
            continue
        # Set this at +20%, similar to vanilla.
        new_max = new_range * 1.2
        max_radar_node.set('range', str(f'{new_max:.0f}'))

    defaults_file.Update_Root(defaults_root)

    # Now look for satellites.
    for class_name, new_range in class_distances.items():

        if class_name == 'satellite':
            macro_name = 'eq_arg_satellite_01_macro'
        elif class_name == 'adv_satellite':
            macro_name = 'eq_arg_satellite_02_macro'
        else:
            continue

        # Load the game file and xml.
        sat_file = Load_File(
            f'assets/props/equipment/satelite/macros/{macro_name}.xml')
        sat_root = sat_file.Get_Root()
        radar_node = sat_root.find(
            f'./macro[@name="{macro_name}"]/properties/radar')
        radar_node.set('range', str(new_range))
        sat_file.Update_Root(sat_root)

    return
コード例 #13
0
def _Build_Ware_Objects():
    '''
    Returns a list of Edit_Objects for all found wares.
    Meant for calling from the Live_Editor.
    '''
    #t_file = Load_File('t/0001-L044.xml')
    # Look up the ware file.
    wares_file = Load_File('libraries/wares.xml')
    xml_root = wares_file.Get_Root_Readonly()

    # Get the ware nodes; only first level children.
    ware_nodes = wares_file.Get_Root_Readonly().findall('./ware')

    start_time = time.time()

    # TODO: maybe condition this on if Settings.disable_threading is
    # set or not.
    if 1:
        '''
        Try out multiprocessing to speed this up.

        Observations, letting python split up ware nodes"
        - Time goes from ~20 to ~30 seconds with 1 worker.
        - Down to ~10 seconds with 6 workers; not much gain.

        To possibly reduce data copy overhead, split up the ware nodes
        manually into lists, and send a single full list to each worker.
        - Down to 7-8 seconds from doing this.
        - Still not great, but more than 2x speedup, so it's something.
        - Note: for different process counts, best was at system
          max threads, with higher counts not losing much time.
        - Can let the Pool handle the thread counting automatically,
          and it does get close, though that doesn't help with picking
          the work unit size.
        - Update: after making production nodes conditional, normal
          runs went 20 to ~4.5 seconds, and this went down to ~2.5.
        
        Later observations:
        - The node ids tagged onto xml element tails seem to be
          transferred okay through pickling, except the one on the
          root node that has to be pruned.

        - The file system appears to get completely replicated for
          every thread, such that the wares file gets node ids
          applied once globally and once per thread.
          The global node ids are higher than the threads since they
          are offset somewhat by any prior loaded xml, while the threads
          all start from 0.

        - This node id discrepency means the loaded elements mess up
          the live editor patch matching, where editing maja snails
          ends up changing marines.

        - How can the stupid python threading be prevented from making
          such a dumb complete system copy that doesn't even catch
          everything? Eg. it should at least be copying the original
          node ids, not starting from scratch.
          - It seems like it goes:
            - Item gets created with paths
            - Item runs value init
            - Value init calls Load_File, expecting it to be a quick
              dict lookup.
            - Multiprocessing barfs on itself and makes a new copy
              of the file system that does not have the wanted
              file loaded, and has to reload it from disk (with
              diff patching).

        - Workaround: change how object building works, such that
          items are linked directly to their source game file and
          do not have to do a file system load.
          Further, tweak the pickler to keep the tag on the top
          element copied.
          Result: things seem to work okay now.
        '''

        # Pick the process runs needed to do all the work.
        # Leave 1 thread free for system stuff.
        num_processes = max(1, cpu_count() - 1)
        max_nodes_per_worker = len(ware_nodes) // num_processes + 1
        slices = []
        start = 0
        while start < len(ware_nodes):
            # Compute the end point, limiting to the last node.
            end = start + max_nodes_per_worker
            if end > len(ware_nodes):
                end = len(ware_nodes)
            # Get the slice and store it.
            slices.append(ware_nodes[start:end])
            # Update the start.
            start = end

        # Use a starmap for this, since it needs to pass both the
        # wares file and the ware node. Starmap will splat out
        # the iterables.
        inputs = [(slice, wares_file) for slice in slices]

        pool = Pool()  #processes = num_processes)
        ware_edit_objects = sum(pool.starmap(
            _Create_Objects,
            inputs,
        ), [])

    else:
        # Single thread style.
        ware_edit_objects = _Create_Objects(ware_nodes, wares_file)

    Print(
        'Ware Edit_Objects creation took {:0.2f} seconds'.format(time.time() -
                                                                 start_time))

    return ware_edit_objects
コード例 #14
0
def Decrease_Fog(empty_diffs = 0):
    '''
    Reduce fog density to increase fps.
    '''
    '''
    Test in heart of acrymony fog cloud, cockpit hidden, which has
    baseline density of 1.0 using p1fogs.fog_04_alpha_dim material.

    1.0: 8.5 fps
    0.5: 15
    0.3: 25
    0.2: 51
    0.1: 72
    0.0: 95

    Since 51 fps is decently playable, aim for an 80% reduction at 1.0 fog.
    To keep relative fog amounts sorted, all fogs will be scaled, but by
    a reduced % at lower base densities.

    Note: high yield ore/silicon region has 2.0 density.

    Idea 1:
        new_density = density * (1 - 0.8 * density)
        1.0 -> 0.2
        0.5 -> 0.3
        Reject, doesn't maintain ordering.

    Idea 2:
        new_density = density * (1 - 0.8 * (density^0.25))
        1.0 -> 0.2
        0.5 -> 0.16
        0.2 -> 0.09
        Stringer reduction below 0.1 than wanted.

    Idea 3:
        if density < 0.1: new_density = density
        else = (d-0.1) * (1 - 0.9 * ((d-0.1)^0.10)) + 0.1
        Linear below 0.1
        Pretty smooth 0.1 to 1.0 (goes up to ~0.2).

    Update: X4 4.0 changes to volumetric fog. While defined similarly,
    densityfactors are a bit odd, sometimes being the same as the old
    commented out fog entries, other times being 1/10th. Also, these
    new entries add a "multiplier" term of unclear intent.
    Highest observed vf densities are:
        1x 1.0 (heart of acrimony), mult 0.05
        1x 0.7, mult 1.0
        1x 0.6, undef
        2x 0.5, mult 0.15, undef
        6x 0.3, mult 1.0, undef
    Note: xsd indicates undefined mult is 0, though could be wrong as it
    is unfinished for vf.

    TODO: revisit; fog perf in 4.0 is much improved, but maybe need to test
    with AA turned on.

    Note: discontinuing this mod for now.
    '''
    game_file = Load_File('libraries/region_definitions.xml')
    xml_root = game_file.Get_Root()

    # Different scalings are possible.
    # This will try to somewhat preserve relative fog amounts.

    # Note: in X4 3.3, fog was part of a "positional" node with an
    # attribute "ref='fog_outside_...' term. In X4 4.0 this has changed
    # to be "volumetricfog" nodes.
    #for positional in xml_root.xpath('.//positional'):
    #    # Skip non-fog
    #    if not positional.get('ref').startswith('fog_outside_'):
    #        continue
    for positional in xml_root.xpath('.//volumetricfog'):

        # Density is between 0 and 1.
        density = float(positional.get('densityfactor'))

        if density < 0.1:
            continue

        # Preserve the lower 0.1.
        d = density - 0.1
        # Get the reduction factor; limit to 0.9.
        reduction = min(0.9, 0.9 * (d ** 0.10))
        # Scale it.
        d = d * (1 - reduction)
        # Add 0.1 back in.
        new_density = d + 0.1

        #print(f'{density:0.2f} -> {new_density:0.2f}')

        positional.set('densityfactor', f'{new_density:.4f}')

    # Testing a fix for second contact 2 fog cutting out.
    # Note: tests failed to help. TODO: move this off to a testing mod.
    if 0:
        # Try removing the minnoise from the region.
        second_contact_region =  xml_root.find('./region[@name="region_cluster_13_sector_001"]')
        second_contact_region.set('minnoisevalue', '0')
        # The above doesnt prevent cutout. Try the falloff values.
        for step in second_contact_region.xpath('./falloff/lateral/step'):
            if step.get('value') == '0.0':
                step.set('value', '0.1')
        for step in second_contact_region.xpath('./falloff/radial/step'):
            if step.get('value') == '0.0':
                step.set('value', '0.1')

    game_file.Update_Root(xml_root)

    return
コード例 #15
0
def Check_Extension(extension_name,
                    check_other_orderings=False,
                    return_log_messages=False):
    '''
    Checks an extension for xml diff patch errors and dependency errors.
    Problems are printed to the console.
    Returns True if no errors found, else False.

    Performs up to three passes that adjust extension loading order:
    in alphabetical folder order, as early as possible (after its
    dependencies), and as late as possible (after all other extensions 
    that can go before it).

    * extension_name
      - Name (folder) of the extension being checked.
      - May be given in original or lower case.
      - This should match an enabled extension name findable on
        the normal search paths set in Settings.
    * check_other_orderings
      - Bool, if True then the 'earliest' and 'latest' loading orders
        will be checked, else only 'alphabetical' is checked.
      - These are recommended to identify where dependencies should
        be added to extensions, to protect against other extensions
        changing their folder name and thereby their loading order.
      - Defaults to False, to avoid printing errors that won't be
        present with the current extension order.
    * return_log_messages
      - Bool, if True then instead of the normal True/False return,
        this will instead return a list of logged lines that
        contain any error messages.
      - Does not stop the normal message Prints.
    '''
    # TODO: think about also checking later extensions to see if they
    #  might overwrite this extension.

    Print('')
    Print('Checking extension: {}'.format(extension_name))

    # Lowercase the name to standardize it for lookups.
    extension_name = extension_name.lower()

    # Success flag will be set false on any unexpected message.
    success = True

    # Pull out the source_reader; this also initializes it if needed.
    source_reader = File_Manager.File_System.Get_Source_Reader()

    # Verify the extension name is valid.
    if extension_name not in source_reader.extension_source_readers:
        raise AssertionError(
            'Extension "{}" not found in enabled extensions: {}'.format(
                extension_name, sorted(source_reader.Get_Extension_Names())))

    # Look up the display name of the extension, which might be used
    # in some messages being listened to.
    extension_display_name = source_reader.extension_source_readers[
        extension_name].extension_summary.display_name

    # Handle logging messages during the loading tests.
    # Do this by overriding the normal log function.
    # Keep a history of messages seen, to avoid reprinting them when
    # the loading order is switched.
    messages_seen = set()

    # Keep a list of lines seen, to possibly return.
    logged_messages = []

    # For name checks, use re to protect against one extension name
    # being inside another longer name by using '\b' as word edges;
    # also add a (?<!/) check to avoid matching when the extension
    # name is in a virtual_path (always preceeding by a '\').
    # Note: the extension_name could have special re characters in it;
    #  can use re.escape to pre-format it.
    # Use (a|b) style to match both forms of the extension name.
    re_name = r'(?<!/)\b({}|{})\b'.format(re.escape(extension_name),
                                          re.escape(extension_display_name))

    def Logging_Function(message):

        # Detect if this extension has its name in the message.
        this_ext_name_in_message = re.search(re_name, message)

        # Want to skip messages based on diff patches by other
        # extensions. Can check the ext_currently_patching attribute
        # of the source_reader for this.
        if (source_reader.ext_currently_patching != None
                and source_reader.ext_currently_patching != extension_name
                # As a backup, don't skip if this extension's name is in
                # the message for some reason (though that case isn't really
                # expected currently).
                and not this_ext_name_in_message):
            return

        # Skip dependency errors from other extensions.
        # TODO: think of a smarter way to do this that can safely ignore
        # messages like this without ignoring those caused by this extension.
        # (Perhaps don't rely on the extension resort to catch these,
        #  but a specific extension checker.)
        if not this_ext_name_in_message:
            for skip_string in [
                    'duplicated extension id', 'missing hard dependency',
                    'multiple dependency matches'
            ]:
                if skip_string in message:
                    return

        if message in messages_seen:
            return
        if 'Error' in message or 'error' in message:
            messages_seen.add(message)
            nonlocal success
            success = False

            # Record the message, if requested.
            if return_log_messages:
                logged_messages.append(message)

            # Print with an indent for visual niceness.
            Print('  ' + message)
        return

    # Connect the custom logging function.
    Plugin_Log.logging_function = Logging_Function

    # Set up the loading orders by adjusting priority.
    # -1 will put this first, +1 will put it last, after satisfying
    # other dependencies. 0 will be used for standard alphabetical,
    # which some mods may rely on.
    priorities = [0]
    if check_other_orderings:
        priorities += [-1, 1]

    # Loop over sorting priorities.
    for priority in priorities:
        if priority == 0:
            Print('  Loading alphabetically...')
        elif priority == -1:
            Print('  Loading at earliest...')
        else:
            Print('  Loading at latest...')

        # Resort the extensions.
        # This will also check dependencies and for unique extension ids.
        source_reader.Sort_Extensions(priorities={extension_name: priority})

        # TODO: maybe think about doing a dependency version check as well,
        # but that isn't very important since x4 will catch those problems,
        # so this tool can somewhat safely assume they will get dealt with
        # by the user.

        # Loop over all files in the extension.
        for virtual_path in source_reader.Gen_Extension_Virtual_Paths(
                extension_name):

            # Caught exception.
            exception = None

            # The path could be to an original file, or to a patch on an
            # existing file.  Without knowing, need to try out both cases
            # and see if either works.
            # Start by assuming this is an original file.
            try:
                Load_File(virtual_path,
                          test_load=True,
                          error_if_unmatched_diff=True)

            # If it was a diff with no base file, catch the error.
            except Unmatched_Diff_Exception:

                # Pop off the extensions/mod_name part of the path.
                _, _, test_path = virtual_path.split('/', 2)

                # Note: some mods may try to patch files from other mods that
                # aren't enabled. This could be an error or intentional.
                # Here, only consider it a warning; explicit dependencies
                # should be caught in the content.xml dependency check.
                # Check if this path is to another extension.
                error_if_not_found = True
                if test_path.startswith('extensions/'):
                    error_if_not_found = False

                # Do a test load; this preserves any prior loads that
                # may have occurred before this plugin was called.
                try:
                    game_file = Load_File(
                        test_path,
                        test_load=True,
                        error_if_not_found=error_if_not_found)
                    if game_file == None:
                        Print(
                            '  Warning: could not find file "{test_path}"; skipping diff'
                        )

                # Some loading problems will be printed to the log and then
                # ignored, but others can be passed through as an exception;
                # catch the exceptions.
                # TODO: maybe in developer mode reraise the exception to
                # get the stack trace.
                except Exception as ex:
                    exception = ex

            except Exception as ex:
                exception = ex

            # Did either attempt get an exception?
            if exception != None:
                # Pass it to the logging function.
                Logging_Function(
                    ('Error when loading file {}; returned exception: {}'
                     ).format(virtual_path, exception))

    Print('  Overall result: ' + ('Success' if success else 'Error detected'))

    # Detach the logging function override.
    Plugin_Log.logging_function = None

    # Return the messages if requested, else the success flag.
    if return_log_messages:
        return logged_messages
    return success
コード例 #16
0
def Clean_Dirty_Glass():
    '''
    Cleans the dirty glass on ship windows.
    '''
    '''
    It looks like the common element in window dirt is the material, eg.:
    '<connection name="Connection21" tags="part nocollision iklink detail_s " parent="anim_cockpit_frame">
    '  ...
    '  <parts>
    '    <part name="detail_xl_glass_inside_dirt">
    '      <lods>
    '        <lod index="0">
    '          <materials>
    '            <material id="1" ref="p1effects.p1_window_trim_01"/>

    ' <connection name="Connection62" tags="part detail_s forceoutline noshadowcaster platformcollision ">
    '   ...
    '   <parts>
    '     <part name="fx_glass_inside">
    '       <lods>
    '         <lod index="0">
    '           <materials>
    '             <material id="1" ref="p1.cockpit_glass_inside_01"/>
    

    The "dirt" in the name is not consistent.
    "p1effects.p1_window_trim_01" may be consistent; it is common in the
    ships sampled.
    Or is the trim just the outside of the window?  Unclear.


    Testing:
        p1effects.p1_window_trim_01
            Haze removed from the edges of the window, around the model
            cockpit frame.
        p1.cockpit_glass_inside_01
            Haze removed from most of the window.
            There is an awkward cuttoff high on the screen where haze reappears.
            May remove some collision? Can seem to walk into the window a bit.
        p1.cockpit_glass_outside_02
            Haze removed from high part of the window, not main part.
        arcp1.cp1_modes
            No change noticed.

    If all haze removed, there is no collision for the window, and the player
    can fall out of the ship.


    Note: removing just the material node from the connection didn't
    have any effect; needed to remove the whole connection.
    
    Suggested to try replacing material with p1effects.p1_chair_ui.
    Maybe try dummy.transparent instead.
    Results: neither had any effect (even with a restart).
    In discussion with others, the material itself may be defined as part
    of the model, and only relies on the connection being present.

    Ultimately, solved below with edit to materials library instead.
    '''
    #ship_macro_files = File_System.Get_All_Indexed_Files('macros','ship_*')
    #ship_files  = File_System.Get_All_Indexed_Files('components','ship_*')

    # Test with the gorgon.
    #ship_files = [Load_File('assets/units/size_m/ship_par_m_frigate_01.xml')]
    #ship_files = []

    #for game_file in ship_files:
    #    xml_root = game_file.Get_Root()
    #
    #    for mat_name in [
    #        #'p1effects.p1_window_trim_01',
    #        #'p1.cockpit_glass_inside_01',
    #        #'p1.cockpit_glass_outside_02',
    #        #'arcp1.cp1_modes',
    #    ]:
    #
    #        results = xml_root.xpath(
    #            ".//connection[parts/part/lods/lod/materials/material/@ref='{}']".format(mat_name))
    #        if not results:
    #            continue
    #
    #        for conn in results:
    #
    #            mat = conn.find('./parts/part/lods/lod/materials/material')
    #            # Try removing the mat link.
    #            #mat.getparent().remove(mat)
    #            # Try editing the mat link.
    #            mat.set('ref', 'dummy.transparent')
    #
    #            # 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)
    '''
    With advice from others looking at the model, there is a reference
    to the cockpit_glass_inside_02 material. Perhaps this can be
    handled by editing the materials.xml to play with that.

    ' <material name="cockpit_glass_inside_02" shader="p1_glass.fx" blendmode="ALPHA8_SINGLE" preview="none" priority="-1">
    '   <properties>
    '     <!--property type="Color" name="diffuse_color" r="200" g="200" b="200" a="100" value="(color 1 1 2)" /-->
    '     <property type="BitMap" name="diffuse_map" value="assets\textures\fx\ships_cockpit_glass_inside_02_diff" />
    '     <property type="BitMap" name="smooth_map" value="assets\textures\fx\ships_cockpit_glass_inside_02_spec" />
    '     <property type="BitMap" name="specular_map" value="assets\textures\fx\ships_cockpit_glass_inside_02_spec" />
    '     <property type="BitMap" name="normal_map" value="assets\textures\fx\ships_cockpit_glass_inside_02_normal" />
    '     <property type="Float" name="normalStr" value="1.0" />
    '     <property type="Float" name="environmentStr" value="0.1" />
    '     <property type="Float" name="envi_lightStr" value="0.40" />
    '     <property type="Float" name="smoothness" value="0.2" />
    '     <property type="Float" name="metalness" value="0.0" />
    '     <property type="Float" name="specularStr" value="0.1" />
    '   </properties>
    ' </material>
    
    Note: in testing, mat lib diff patches only evaluate at game start,
    so need to restart between changes.
    (Test was to delete the p1 collection, turning the game purple; game
    stayed purple after undoing the diff patch deletion and reloading
    the save.)

    Tests:
        Removing material nodes
            Purple textures.
        Removing the bitmaps
            Generic white or grey coloring.
        Maybe try replacing it with the data from dummy.transparent?
            Textures replaced with the custom nuke icon spammed out
            around the trim, and central window is a stretched out
            and red-shifted copy.
            Why??
        Try replacing with p1effects.p1_chair_ui.
            Get a random texture tiled out again, some purplish paranid
            looking keyboard like thing?
        Try adding/changing attributes to look for some way to add transparency.
            No noticed effects for properties tried.
        Remove just the normal bitmap.
            No noticed effect.
        Change all bitmaps to use assets\textures\fx\transparent_diff
            Success!

    Note: removing the outside glass texture means there is no
    apparent cockpit glass when viewed from outside. So try to leave
    the outside glass intact.
    '''

    if 1:
        material_file = Load_File('libraries/material_library.xml')
        xml_root = material_file.Get_Root()

        #dummy_node = xml_root.find(".//material[@name='transparent']")
        #dummy_node = xml_root.find(".//material[@name='p1_chair_ui']")

        ## New properties to add.
        #propeties = [
        #    etree.fromstring('<property type="Float" name="diffuseStr" value="1.0" />'),
        #    etree.fromstring('<property type="Float" name="color_dirtStr" value="0.0" />'),
        #    etree.fromstring('<property type="Float" name="translucency"  value="0.0" />'),
        #    etree.fromstring('<property type="Float" name="trans_scale"   value="0.0" />'),
        #    etree.fromstring('<property type="Color" name="diffuse_color" r="200" g="200" b="200" a="0" value="(color 1 1 2)" />'),
        #    ]

        for mat_name in [
                'cockpit_glass_inside_01',
                'cockpit_glass_inside_02',
                #'cockpit_glass_outside_01',
                #'cockpit_glass_outside_02',
                'p1_window_trim_01',
                # TODO: other trims?
                #'p1_window_trim_02', - exists, unclear which ships use it.
                # p1_window_trim_03  - Added in 3.10hf1b1, no mat entry yet?
        ]:
            mat_node = xml_root.xpath(
                ".//material[@name='{}']".format(mat_name))
            assert len(mat_node) == 1
            mat_node = mat_node[0]

            # Removing bitmaps - failure, saturated colors
            #for bitmap in mat_node.xpath("./properties/property[@type='BitMap']"):
            #    bitmap.getparent().remove(bitmap)

            # Removing mat node - failure, purple error
            #mat_node.getparent().remove(mat_node)

            # Replacing with transparent node copy.
            # Note: deepcopy here doesn't traverse up to the parent.
            # - failure, grabs random textures to fill in.
            #new_node = deepcopy(dummy_node)
            #new_node.set('name', mat_node.get('name'))
            #mat_node.addnext(new_node)
            #mat_node.getparent().remove(mat_node)

            ## Try adding extra properties as children.
            #for property in propeties:
            #    new_prop = deepcopy(property)
            #    # If one already exists, delete the old one.
            #    old_props = mat_node.xpath("./properties/property[@name='{}']".format(new_prop.get('name')))
            #    for old_prop in old_props:
            #        old_prop.getparent().remove(old_prop)
            #    # Append the new one.
            #    mat_node.append(new_prop)

            # Remove the normal bitmap only. - no effect
            #for bitmap in mat_node.xpath("./properties/property[@name='normal_map']"):
            #    bitmap.getparent().remove(bitmap)

            # 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)

    return
コード例 #17
0
def Make_Extension_Content_XML(
        name        = None,
        author      = None,
        version     = None,
        save        = False,
        sync        = False,
        enabled     = True,
        description = None,
    ):
    '''
    Adds an xml file object defining the content.xml for the top
    of the extension.  Common fields can be specified, or left as defaults.

    * name
      - String, display name; defaults to extension folder name.
    * author
      - String, author name; defaults to X4 Customizer.
    * version
      - String, version code; defaults to customizer version.
    * save
      - Bool, True if saves made with the extension require it be enabled.
    * sync
      - Bool.
    * enabled
      - Bool, True to default to enabled, False disabled.
    * description
      - String, extended description to use, for all languages.
      - Newlines are automatically converted to "&#10;" for display in-game.
    '''
    # If the content exists already, return early.
    if Load_File('content.xml', error_if_not_found = False) != None:
        return
    
    if not version:
        # Content version needs to have 3+ digits, with the last
        #  two being sub version. This doesn't mesh will with
        #  the version in the Change_Log, but can do a simple conversion of
        #  the top two version numbers.
        version_split = Framework.Change_Log.Get_Version().split('.')
        # Should have at least 2 version numbers, sometimes a third.
        assert len(version_split) >= 2
        # This could go awry on weird version strings, but for now just assume
        # they are always nice integers, and the second is 1-2 digits.
        version_major = version_split[0]
        version_minor = version_split[1].zfill(2)
        assert len(version_minor) == 2
        # Combine together.
        version = version_major + version_minor

    # If a description given, format slightly.
    if description:
        # Need to use "&#10;" for newlines in-game.
        description = description.replace('\n', '&#10;')
    else:
        description = ' '

    # Set the ID based on replacing spaces.
    this_id = Settings.extension_name.replace(' ','_')

    # Set up the root content node.
    content_node = ET.Element(
        'content',
        attrib = {
            # Swap spaces to _; unclear on if x4 accepts spaces.
            'id'         : this_id,
            'name'       : name if name else Settings.extension_name,
            'author'     : author if author else 'X4_Customizer',
            'version'    : version,
            'date'       : File_System.Get_Date(),
            # TODO: maybe track when changes are save breaking, eg. if
            #  adding in new ships or similar. Though in practice, it
            #  may be best to always keep this false, and offer transforms
            #  that can undo breaking changes safely before uninstall.
            'save'       : 'true' if save else 'false',
            'sync'       : 'true' if sync else 'false',
            'enabled'    : 'true' if enabled else 'false',
            'description': description,
            })

    
    # Fill out language description text.
    # This loops over all language ids noticed in the cat/dat t files.
    for lang_id in ['7','33','34','39','44','48','49','55','81','82','86','88']:
        # Set up a new text node.
        # TODO: per-language descriptions.
        text_node = ET.Element('language', language = lang_id, description = description)

        # Add to the content node.
        content_node.append(text_node)

    # Record it.
    File_System.Add_File( XML_File(
        xml_root = content_node,
        virtual_path = 'content.xml',
        modified = True))
    
    # Use shared code to fill in dependencies.
    Update_Content_XML_Dependencies()
    return
コード例 #18
0
def Patch_Shader_Files(shader_names, testmode=False):
    '''
    Edit asteroid shader to support transparency.

    * shader_names
       - Names of shader ogl files to edit.
    * testmode
      - Sets asteroid color to white when unfaded, black when fully faded,
        for testing purposes.
    '''
    '''
    The asteroid shader, and ogl file linking to v/vh/etc., doesn't normally
    support camera fade. However, fading rules are in the common.v file,
    and fade parameters can be added to the shaer ogl.
    Note: ogl is xml format.

    Note:
    .v : vertex shader
    .f : fragment shader (most logic is here)

    Note: multiple ogl files can link to the same f shader file.
    Ensure a) that each shader file is modified once, and b) that each ogl
    file using a shader is also modified to fill in fade range defaults.

    Note: some ogl files are empty; skip them (will have load_error flagged).
    
    Note: in testing, other users of these shaders tend to error,
    apparently tracing back to the V_cameraposition or IO_world_pos
    values (at a guess), which even when not executed still cause a problem.

        It is possible the other ogl spec files do not have corresponding
        defines for the above values to be present.

        Possible fixes:
        - Duplicate all shaders being edited, link their ogl to the
            unique versions, so original ogl/shader files are unchanged.
        - Modify other ogl files to define whatever is needed to ensure
            these variables are available.
        - Modify the common header to ensure this vars are available.
        - Regenerate these vars here (if possible).
    '''
    # From ogl files, get their fragment shader names.
    shader_f_names = []

    # Go through ogl files and uniquify/modify them.
    for shader_name in shader_names:
        shader_ogl_file = Load_File(f'shadergl/high_spec/{shader_name}')
        # Skip empty files.
        if shader_ogl_file.load_error:
            continue

        # Copy out the root.
        xml_root = shader_ogl_file.Get_Root()

        # Grab the fragment shader name.
        shader_node = xml_root.find('./shader[@type="fragment"]')
        shader_f_name = shader_node.get('name')
        # Record it.
        if shader_f_name not in shader_f_names:
            shader_f_names.append(shader_f_name)
        # Uniquify it.
        shader_f_name = shader_f_name.replace('.f', '_faded.f')
        shader_node.set('name', shader_f_name)

        # Want to add to the properties list.
        properties = xml_root.find('./properties')

        # -Removed; these defines don't seem to work, and may be undesirable
        # anyway since they might conflict with existing logic/defines,
        # eg. doubling up fade multipliers.
        # Add defines for the camera distance and fade.
        # Put these at the top, with other defines (maybe doesn't matter).
        #for field in ['ABSOLUTE_CAMERA_FADE_DISTANCE', 'FADING_CAMERA_FADE_RANGE']:
        #    property = etree.Element('define', name = field, value = '/*def*/')
        #    properties.insert(0, property)
        #    assert property.tail == None

        # The fade calculation will use these properties.
        # Assign them initial defaults in the shader (which also covers possible
        # cases where the shader is used by materials that weren't customized).
        # Note: These default values are not expected to ever be used.
        for field, value in [('ast_camera_fade_range_start', '19900000.0'),
                             ('ast_camera_fade_range_stop', '20000000.0')]:
            property = etree.Element('float', name=field, value=value)
            properties.append(property)
            assert property.tail == None

        # Generate a new file for the new shader spec.
        File_System.Add_File(
            XML_File(virtual_path=shader_ogl_file.virtual_path.replace(
                '.ogl', '_faded.ogl'),
                     xml_root=xml_root,
                     modified=True))
        #shader_ogl_file.Update_Root(xml_root)

    # Need to update the .f file that holds the actual shader logic.
    for shader_f_name in shader_f_names:
        shader_f_file = Load_File(f'shadergl/shaders/{shader_f_name}')

        # Do raw text editing on this.
        text = shader_f_file.Get_Text()
        new_text = text

        # Dither based approach. If setting this to 0, experimental code
        # is tried further below.
        if 1:
            '''
            Note: these comments were made in X4 3.3.

            Various attempts were made to access proper transparency through
            the alpha channel, but none worked.
            Observations:
            - OUT_Color controls reflectivity (using low-res alt backdrop),
              though in theory should be actual color.
            - OUT_Color1 controls ?
            - OUT_Color2 controls actual color (when used?)
            - Vulkan lookups suggest transparency might depend on an alpha channel,
              and would be super expensive to compute anyway.

            Instead, use a dithering approach, showing more pixels as it gets closer.
            Can use the "discard" command to throw away a fragment.

            gl_FragCoord gives the screen pixel coordinates of the fragment.
            Vanilla code divides by V_viewportpixelsize to get a Percentage coordinate,
            but that should be unnecessary.

            Want to identify every 1/alpha'th pixel.
            Eg. alpha 0.5, want every 2nd pixel.
                v * a = pix_selected + fraction
                If this just rolled over, pick this pixel.
                if( fract(v*a) >= a) discard;
                If a == 1, fract will always be 0, 0 >= 1, so discards none.
                If a == 0, fract will always be 0, 0 >= 0, so discards all.


            Note: dithering on rotating asteroids creates a shimmer effect,
            as lighter or darker areas of the asteroid rotate into display pixels.
            Further, since dither is calculated by distance, and different points
            on the asteroid are at different distances, the pixels shown are
            also moving around (since asteroid isn't perfectly round).

                To mitigate shimmer, also adjust the object coloring.
                A couple options:
                - Dim to black. This is similar to some alpha examples. Would work
                  well on a dark background (common in space).
                - Average to the asteroid base color. This would require specifically
                  setting the base color for each asteroid type (brown for ore,
                  blueish for ice, etc.). Avoids a black->blueish transition on ice.
                  May look worse against a dark backdrop (eg. one blue/white pixel).

                This code will go with generic color dimming for now.
                Result: seems to help, though not perfect.


            Further shimmer mitigation is possible by reducing volatility of
            the camera depth.
                In reading, it doesn't appear pixel/fragment shaders normally
                support distance to the original object, only to their
                particular point on the surface.

                But, if the cam distance (or computed alpha) is rounded off, it
                will create somewhat more stable bins.  There would still be
                a problem when the asteroid surface moves from one bin to another,
                but most of the shimmer should be reduced.

                Rounding the fade factor is probably more robust than cam distance.
                Eg. fade = floor(fade * 100) / 100

                Result: somewhat works, but makes the pixel selection pattern
                really obvious. Where (dist.x+dist.y) is used, gets diagonal
                lines of drawn pixels.


            Cull pixel selection
                This has a problem with creating obvious patterns.
                Cull x < cuttoff || y < cuttoff:
                - Get mesh of dark lines (culled points).
                Cull x < cuttoff && y < cuttoff:
                - Get mesh of shown lines (non-culled points).
                Cull x + y < cuttoff
                - Get diagonal lines.

                TODO: what is a good culling formula that spreads out the points,
                both when mostly faded (first few points shown) and when mostly
                shown (last few points culled).
            
                Patterns are most obvious when zooming in, but can be somewhat
                seen by leaning forward, or identified when turning the camera.

        
            Reverse-shimmer is a somewhat different problem where eg. a blue/white
            ice asteroid that is mostly drawn will have some black background 
            pixels shimmering around and standing out.
           
                This could be addressed by fading in with two steps:
                - Dither region, where more pixels are drawn, all black.
                - Color region, where pixels adjusted from black to wanted color.
            
                Such an approach would also address other shimmer problems above,
                with the caveat that it might be even more sensitive to the
                overall background color (black is good, otherwise bad).

                Note: small asteroids drawing in front of already-visible larger
                asteroids would cause this background discrepency, eg. a small
                ice chunk starts dithering in black pixels when a large ice
                asteroid behind it is blue/white.

                For now, ignore this problem, as solutions are potentially worse.

            TODO: adjust for viewport width (or whatever it was called), so that
            zoomed in views see more of the asteroid draw (reducing obviousness
            of the dither effect when using a zoom hotkey).
        
            TODO: maybe discard if not gl_FrontFacing; probably not important.

            To stabalize pattern when camera turns:
                Use the angle from cam pos to object pos.
                angles = sin((cam.x - obj.x) / (cam.z - obj.z)) + sin((cam.y - obj.y) / (cam.z - obj.z))
                Tweak this based on:
                - Resolution; reduced angle between pixels at higher res.
                - Round to roughly matching up to pixel density.

                What is the angle between pixels?
                Guess: 100 fov at 1650 wide, so 5940 pix in full circle, ~6k.
                Can compute live based on V_viewportpixelsize.x, though fov
                is unclear (maybe based on x/y ratio?).

                In testing, centering on a nav beacon and counting turns of the
                camera to get back to it, it takes 4.3 turns, or 84 degrees.
                Note: trying with the 100 assumption had some dither noise when
                turning the camera, so that was wrong; maybe 84 is right?

                Scaling by resolution: fov = 84 / (10/16) * x/y = 52.5 * x/y

                Does this even need an actual angle?
                Can get the xyz vector from the camera to the object, and adjust
                to be a unit vector:
                    dir = (cam.xyz - obj.xyz)
                    dir = dir / distance(dir)
                That's probably good enough, but how to round for stability?
                May need to use atan anyway to get angles.

                Test result:
                - Looks mostly stable, except some shimmering noticed occasionally,
                notably near the +x axis center.
                - Zooming looks bad, but not much to be done about that.
                - Some shimmer when turning just due to resolution and aliasing,
                similar to general object edges.

            '''
            # Pick number of fade stepping bins, used to reduce shimmer.
            # Should be few enough that steppings don't stand out visually.
            num_bins = 20

            # Copy over the IO_Fade calculation from the common.v, and
            # do any customization. This also allows unique var names, to
            # avoid stepping on existing fade variables (eg. IO_fade).
            # Note: ast_fade is live through the function in testmode, so give
            # it a name likely to be unique.
            new_code = f'''
                float ast_cameradistance = abs(distance(V_cameraposition.xyz, IO_world_pos.xyz));
                float ast_faderange = U_ast_camera_fade_range_stop - U_ast_camera_fade_range_start;
                float ast_fade = 1.0 - clamp(abs(ast_cameradistance) - U_ast_camera_fade_range_start, 0.0, ast_faderange) / ast_faderange;
                ast_fade = round(ast_fade * {num_bins:.1f}) / {num_bins:.1f};
            '''
            # Add in the discard check if not in test mode.
            # Want to avoid diagonal patterns (x+y) in favor of a better scatter.
            if not testmode:
                #new_code += '''
                #    if( fract((gl_FragCoord.x + gl_FragCoord.y) * ast_fade) >= ast_fade)
                #        discard;
                #    '''

                # Make a 2-wide vector for this. Note: vulkan doesn't seem to
                # support something from documentation checked (sqrt? bvec?
                # greaterThanEqual?), so expand out the comparison.
                # Want to keep x/y crossing points, so discard unwanted x and y
                # (OR the check).
                #new_code += '''
                #    if (ast_fade < 0.999){
                #        float ast_fade_sqrt = sqrt(ast_fade);
                #        vec2 ast_factions = fract(gl_FragCoord.xy * ast_fade_sqrt);
                #        if( ast_factions.x >= ast_fade_sqrt || ast_factions.y >= ast_fade_sqrt)
                #            discard;
                #    }
                #    '''

                # Better idea: use the fragment xyz, so the discard pattern doesnt
                # follow the camera angle when turned.
                # If the coordinate is a noisy float (eg. not 2.00000), can use
                # its deep fractional part as a sort of random value.
                # Result: looks good on a still object, but asteroid rotation
                # creates shimmier, so reject.
                #new_code += '''
                #    if (ast_fade < 0.999){
                #        float psuedo_rand = fract((IO_world_pos.x + IO_world_pos.y + IO_world_pos.z) * 16.0);
                #        if( psuedo_rand >= ast_fade)
                #            discard;
                #    }
                #    '''

                # Try to create a random value from the screen xy position.
                # Note: quick reading indicates gpu sin/cos is just 1 cycle.
                # Example of randomizer calculation here:
                # https://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl
                # Works decently, though dither tracks with camera rotation.
                if 0:
                    new_code += '''
                    if (ast_fade < 0.999){
                        float psuedo_rand = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898,78.233))) * 43758.5453);
                        if( psuedo_rand >= ast_fade)
                            discard;
                    }
                    '''

                # More complex case: compute a rounded angle.
                if 1:
                    # Fov is roughly 52.5 * x/y of the resolution.
                    # angle_per_pixel = fov / x
                    # Want to round based on angle_per_pixel.
                    # roundto = 52.5 * x/y / x = 52.5 / y
                    # Switch to radians.
                    round_y_ratio = math.pi * 2 / 360 * 52.5
                    new_code += f'''
                    if (ast_fade < 0.999){{
                        float roundto = {round_y_ratio} / V_viewportpixelsize.y;
                        vec3 dir = V_cameraposition.xyz - IO_world_pos.xyz;
                        // Note: atan is -pi to pi.
                        vec2 angles = vec2(
                            atan(dir.x, dir.z), 
                            atan(dir.y, dir.z)
                            );
                        // Round them.
                        angles = floor((angles / roundto) + 0.5) * roundto;
                        // Get a rand from the angles.
                        // Ideally don't want patterns, eg. circles of discards,
                        // so don't add the angles. Can do what was done above
                        // with xy.
                        float psuedo_rand = fract(sin(dot(angles, vec2(12.9898,78.233))) * 43758.5453);
                        if( psuedo_rand >= ast_fade)
                            discard;
                    }}
                    '''

            # Replace a line near the start of main, for fast discard (maybe slightly
            # more performant).
            # TODO: make the ref line more robust.
            ref_line = 'main()\n{'
            assert new_text.count(ref_line) == 1
            new_text = new_text.replace(ref_line, ref_line + new_code)

            # In test mode, shortcut the ast_fade to the asteroid color.
            # Close asteroids will be white, far away black (ideally).
            # This overwrites the normal asteroid output result.
            if testmode:
                new_code = '''
                    OUT_Color  = half4(0);
                    OUT_Color1 = vec4(0);
                    OUT_Color2 = vec4(ast_fade,ast_fade,ast_fade,0); 
                    '''
                # Replace a late commented line, overriding out_color.
                # TODO: more robust way to catch close of main.
                ref_line = '}'
                new_text = (new_code + ref_line).join(
                    new_text.rsplit(ref_line, 1))

            # When not in test mode, dim the asteroid final color.
            # TODO: does this work for all shaders, eg. color in out_color2?
            # Or perhaps can all color fields (eg. reflectivity) be dimmed
            # equally without impact?
            # Makes everything green tinted?
            if not testmode and 0:
                new_code = '''
                    OUT_Color  = OUT_Color  * ast_fade;
                    OUT_Color1 = OUT_Color1 * ast_fade;
                    OUT_Color2 = OUT_Color2 * ast_fade;
                    '''
                ref_line = '}'
                new_text = (new_code + ref_line).join(
                    new_text.rsplit(ref_line, 1))

            # Based on experimentation below, can play with the
            # asteroid normal/specular to introduce some light transparency,
            # which may help with a smoother fade-in alongside the dither.
            # Set the normal to average between 0 rgb and (1-ast_fade)
            # alpha, and what it would otherwise be, for smoother fade-in.
            #
            # -Removed; in practice, the norm/spec only draws when the
            # asteroid is somewhat closer than its render distance, eg.
            # it will be dither in 40% and then suddenly the semi-clear
            # normals kick in. (Tested on medium roids in Faulty Logic.)
            # TODO: maybe revisit if finding a way to extend
            # normals out to further distances (may require
            # messing with models or something).
            if not testmode and 0:
                new_code = '''
                    OUT_Color.a = (OUT_Color.a + 1 - ast_fade) / 2;
                    OUT_Color.rgb = OUT_Color.rgb * ast_fade;
                    //OUT_Color.a = 1.0;
                    //OUT_Color.rgb = half3(0.0, 0.0, 0.0);
                    '''
                ref_line = '}'
                new_text = (new_code + ref_line).join(
                    new_text.rsplit(ref_line, 1))

        elif 0:
            # Experiment with transparency, hopefully finding out how to make
            # it work in x4 4.0.
            '''
            The F_alphascale property appears to be supplied by the game
            engine. In testing, this appears to start at 0, says 0 for a
            while, then snaps to 1 when halfway to the asteroid (around
            when the asteroid goes fully opaque in egosoft's 4.0 handling).
            Egosofts actual fade-in appears to be done separately, outside
            of the shader, so this isn't useful.

            The region_lodvalues.xsd file indicates that there are
            two contributors to engine-calculated fade, one for time (if
            asteroids spawn in inside their render distance), one for
            distance. The distance version is janky in testing, particularly
            in Faulty Logic, where an asteroid quickly fades in over ~50m
            to ~50% transparent black, then slowly fills in color over ~30km
            (slowly losing transparency), then quickly goes from 80%->100%
            opaque at around 35km. Tested on a mid-sized silicon asteroid.

            This shader edit will be similar to the 3.3 version, calculating
            a smoother fade-in alpha, and will experiment with applying
            it to the asteroid alpha channel (maybe without success).

            Theory of shader operation:
                a) Differred rendering is on
                b) B_deferred_draw is true?
                c) Shader calls macro DEFERRED_OUTPUT which sets colors,
                   passing it the shader computed rgb as GLOW param.
                d) Macro assigns:
                   OUT_Color2 = ColorGlow.rgb, 3
                e) Macro calls other macro, STORE_GBUFFER
                f) STORE_GBUFFER assigns:
                   OUT_Color  = NORMAL.rgb, spec (=0.2)
                   OUT_Color1 = BASECOLOR.rgb, METAL

            Overall testing result: the egosoft transparency setting appears
            to be done separately from the shader, and while shader edits
            could expose some moderate transparency, side effects from 
            haziness just make it look bad (and cannot be hidden since
            asteroids wouldnt go fully transparent).
            Giving up on this path for now to go back to dithering.
            '''
            # Calculate the custom fade.
            # See notes above (on 3.3 version) for explanation.
            new_code = f'''
                float ast_cameradistance = abs(distance(V_cameraposition.xyz, IO_world_pos.xyz));
                float ast_faderange = U_ast_camera_fade_range_stop - U_ast_camera_fade_range_start;
                float ast_fade = 1.0 - clamp(abs(ast_cameradistance) - U_ast_camera_fade_range_start, 0.0, ast_faderange) / ast_faderange;
            '''
            # Take the min of this and the engine supplied value.
            # TODO: retest; remove for  now; alphascale doesnt actually appear
            # to be egosofts engine alpha.
            new_code += f'''
                //ast_fade = min(ast_fade, F_alphascale);
                //ast_fade = F_alphascale;
                ast_fade = 0.5;
            '''

            # Change uses of F_alphascale to instead use ast_fade later
            # in the shader (should just be two uses, one for z depth, other
            # for final output alpha adjustment).
            # (Do this before inserting ast_fade calculation).
            # -Removed for now;
            #new_text = new_text.replace('F_alphascale', 'ast_fade')

            # Replace a line near the start of main, so it is in place before
            # first use of ast_fade.
            # TODO: make the ref line more robust.
            ref_line = 'main()\n{'
            assert new_text.count(ref_line) == 1
            new_text = new_text.replace(ref_line, ref_line + new_code)

            # Apply to the final asteroid color.
            # TODO: does this work for all shaders, eg. color in out_color2?
            # Or perhaps can all color fields (eg. reflectivity) be dimmed
            # equally without impact?
            # Note: DEFERRED_OUTPUT appears to be in use, as in testing
            # a full reapplication of the last OUT_Color line just before
            # the function end causes oddities in asteroid appearance (they
            # show up half transparent at any range).
            new_code = '''
                //OUT_Color  = OUT_Color  * ast_fade;
                //OUT_Color1 = OUT_Color1 * ast_fade;
                //OUT_Color2 = OUT_Color2 * ast_fade;

                // Does nothing.
                //OUT_Color.a = 0.5;
                //OUT_Color.a = OUT_Color.a * ast_fade;

                //-Causes half transparent asteroids, any distance; background
                // is blurred through asteroid.
                // Asteroids also take on a haziness at a distance (seem to be
                // taking on some of the average background color), so they
                // show up dark against light colored background, and light
                // against dark colored background.
                //OUT_Color = half4(finalColor.rgb, ColorBaseDiffuse.a * F_alphascale);
                // Similar but less transparent.
                //OUT_Color = 0.5 * half4(finalColor.rgb, ColorBaseDiffuse.a * F_alphascale);
                // Distant asteroids slightly white/shiny; generally more transparent.
                //OUT_Color = 2 * half4(finalColor.rgb, ColorBaseDiffuse.a * F_alphascale);
                // Similar to above, distant slightly white (maybe background
                // showing through? but whiter than background, perhaps due
                // to blurred background).
                //OUT_Color = half4(finalColor.rgb, ColorBaseDiffuse.a * F_alphascale * 2);
                // Asteroid seems a bit desaturated and bland, but opaque.
                //OUT_Color.rgb = finalColor.rgb;
                // Doing rgb and a separately has the same effect as together,
                // so no oddities with half4.
                //OUT_Color.rgb = finalColor.rgb;
                //OUT_Color.a = ColorBaseDiffuse.a * F_alphascale;
                
                // Opaque but dark asteroids (not black though).
                //OUT_Color = half4(0,0,0,0);
                // Opaque, distant asteroids white, nearby colored normally;
                // zooming in switches white roids to normal (so something
                // to do with rendering angle and not distance).
                //OUT_Color = half4(0.5,0.5,0.5,0.5);
                // Opaque, distant asteroids white again, similar to above.
                //OUT_Color.rgb = half3(0.5,0.5,0.5);
                // Alpha changes did nothing notable.
                //OUT_Color.a = 0.5;
                //OUT_Color.a = 0.05;
                //OUT_Color.a = 0.95;
                //OUT_Color.a = ColorBaseDiffuse.a * F_alphascale;
                // Distant asteroids are lighter colored, normal up close.
                //OUT_Color.a = 10;
                // Distant asteroids even lighter/glowier with blue halo,
                // normal up close.
                //OUT_Color.a = 128;
                // Opaque and extremely shiny at distance, normal up close.
                //OUT_Color.rgb = half3(128,128,128);
                // Distant roids shiny red/green instead of white.
                //OUT_Color.r = 128;
                //OUT_Color.g = 128;

                // Just try to rescale alpha, to narrow down its number.
                //OUT_Color.rgb = finalColor.rgb;
                // Opaque to stars, seemingly, but background fog/nebula
                // show through slightly.
                //OUT_Color.a = 0.1;
                // Modestly transparant, can make out the blurred stars.
                //OUT_Color.a = 0.5;
                // More transparant, but feels like 80%; can still make out
                // the asteroid, and distance asteroids still have a light
                // colored haze to them.
                //OUT_Color.a = 1.0;
                // Similar to above.
                //OUT_Color.a = 1.5;

                // Try to rescale rgb, to narrow down its number.
                //OUT_Color.a = 1.0;
                // Around 80% transparent still, but asteroid has a darker hue.
                //OUT_Color.rgb = half3(0.0, 0.0, 0.0);
                // Asteroids on one half of the sector are bright white,
                // at any distance, based on player view point (eg. can strafe
                // to switch an asteroid from normal to white gradually).
                //OUT_Color.rgb = half3(1.0, 1.0, 1.0);
                // Giving up here; it might be negative or something, but
                // seems like a dead end with the haziness.

                // Try out the other color alphas.
                // No noticed effect from this.
                //OUT_Color2.a = 0.5;
                // Nor from this.
                //OUT_Color1.a = 0.5;
                '''
            ref_line = '}'
            new_text = (new_code + ref_line).join(new_text.rsplit(ref_line, 1))

            # In test mode, shortcut the ast_fade to the asteroid color.
            # Close asteroids will be white, far away black (ideally).
            # This overwrites the normal asteroid output result.
            if testmode:
                new_code = '''
                    OUT_Color  = half4(0);
                    OUT_Color1 = vec4(0);
                    OUT_Color2 = vec4(ast_fade,ast_fade,ast_fade,0); 
                    '''
                # Replace a late commented line, overriding out_color.
                # TODO: more robust way to catch close of main.
                ref_line = '}'
                new_text = (new_code + ref_line).join(
                    new_text.rsplit(ref_line, 1))

        # Uniquify the file.
        File_System.Add_File(
            Text_File(virtual_path=shader_f_file.virtual_path.replace(
                '.f', '_faded.f'),
                      text=new_text,
                      modified=True))

    return
コード例 #19
0
def Apply_Live_Editor_Patches(file_name=None):
    '''
    This will apply all patches created by hand through the live editor
    in the GUI.
    This should be called no more than once per script, and currently
    should be called before any other transforms which might read
    the edited values.
    Pending support for running some transforms prior to hand edits.

    * file_name
      - Optional, alternate name of a json file holding the 
        Live_Editor generated patches file.
      - Default uses the name in Settings.
    '''
    # Make sure the live editor is up to date with patches.
    # TODO: think about how safe this is, or if it could overwrite
    # meaningful existing state.
    Live_Editor.Load_Patches(file_name)

    # TODO: fork the xml game files at this point, keeping a copy
    # of the pre-patch state, so that live editor pages loaded
    # after this point and properly display the xml version from
    # before the hand edits and later transforms.
    # This may need to be done wherever pre-edit transform testing
    # is handled.

    # Work through the patches.
    # To do a cleaner job loading/saving game files, categorize
    # the patches by virtual_path first.
    path_patches_dict = defaultdict(list)
    for patch in Live_Editor.Get_Patches():
        path_patches_dict[patch.virtual_path].append(patch)

    for virtual_path, patch_list in path_patches_dict.items():
        # Note: if patches get out of date, they may end up failing at
        # any of these steps.

        # Load the file.
        game_file = Load_File(virtual_path)
        if game_file == None:
            Plugin_Log.Print(('Warning: Apply_Live_Editor_Patches could'
                              ' not find file "{}"').format(virtual_path))
            continue

        # Modify it in one pass.
        root = game_file.Get_Root()

        for patch in patch_list:
            # Look up the edited node; assume just one xpath match.
            nodes = root.xpath(patch.xpath)
            if not nodes:
                Plugin_Log.Print(('Warning: Apply_Live_Editor_Patches could'
                                  ' not find node "{}" in file "{}"').format(
                                      patch.xpath, virtual_path))
                continue
            node = nodes[0]

            # Either update or remove the attribute.
            # Assume it is safe to delete if the value is an empty string.
            if patch.value == '':
                if patch.attribute in node.keys():
                    node.attrib.pop(patch.attribute)
            else:
                node.set(patch.attribute, patch.value)

        # Put changes back.
        # TODO: maybe delay this until all patches get applied, putting
        # back before returning.
        game_file.Update_Root(root)

    return