Nexus-Mods / Vortex

Vortex Development
GNU General Public License v3.0
911 stars 135 forks source link

ASSISTANCE REQUEST - Changing File Name Based on Load Order for Helldivers 2 #16582

Open ChemGuy1611 opened 1 week ago

ChemGuy1611 commented 1 week ago

I am attempting to automatically rename files for Helldivers 2 based on Load Order position. I have made some attempts at this, but I am having difficulties finding a method that will work.

The files I'm working with look like this:

const PATCH_FILE1 = "9ba626afa44a3aa3.patch_0";
const PATCH_FILE2 = "9ba626afa44a3aa3.patch_0.gpu_resources";
const PATCH_FILE3 = "9ba626afa44a3aa3.patch_0.stream";

What I need to do is change the "0" after patch for each file to the number corresponding to its mod's position in the load order. I have the load order page implemented in the extension for the mods of this type (PATCH_ID), and I am currently attempting to use the callback function within context.registerLoadOrderPage to do the file renaming.

I am using similar code to the Unreal Engine Mod Installer to get an index of the mod's position in the load order with the function loadOrderSuffix.

let previousLO;
  context.registerLoadOrderPage({
    gameId: spec.game.id,
    gameArtURL: path.join(__dirname, spec.game.logo),
    preSort: (items, direction) => preSort(context.api, items, direction),
    filter: mods => mods.filter(mod => mod.type === PATCH_ID),
    displayCheckboxes: false,
    callback: (loadOrder, mods) => {
      if (previousLO === undefined) previousLO = loadOrder;
      if (loadOrder === previousLO) return;
      renamePatchFiles(context.api, loadOrder, mods);
      context.api.store.dispatch(actions.setDeploymentNecessary(spec.game.id, true));
      previousLO = loadOrder;
    },
    createInfoPanel: () =>
    context.api.translate(`Drag and drop the patch mods on the left to change the order in which they load. ${spec.game.name} loads patch mods in numerical order, so Vortex suffixes `
    + 'the file names with ".patch0, .patch1, .patch2, ..." to ensure they load in the order you set here. '
    + 'The number in the left column represents the overwrite order. The changes from mods with higher numbers will take priority over other mods which make similar edits.'),
  });
//Rename .patch0 files based on load order position
async function renamePatchFiles(api, loadOrder, mods) {
  // Check we are managing HD2
  const state = api.getState();
  const gameId = selectors.activeGameId(state);
  if (gameId !== GAME_ID) return;
  //const stagingFolder = selectors.installPathForGame(state, GAME_ID);

  // Get the mod data
  const MODS_FILTERED = state.persistent.mods[GAME_ID];
  //const MODS_FILTERED = mods.filter(mod => (mod.type === PATCH_ID));
  if (!MODS_FILTERED) return;

  // Read the contents of the staging folder for this mod
  //const modPath = path.join(stagingFolder, modId);

  //util.copyFileAtomic(`srcPath`, `destPath`)

  //Rename the files
  try {
    MODS_FILTERED.forEach(
        (mod, idx) => {
            let pos_adj = 1;
            loadOrderSuffix(context.api, mod);
            const MOD_FILES = mods[mod.id].files[mod.id];
            //const MOD_FILES = mod.files;
            MOD_FILES.forEach(
                (file, idx) => {
                    const MOD_FILE = MOD_FILES[idx];
                    const FILE_NAME = path.basename(MOD_FILE);
                    const FILE_NAME_NEW = FILE_NAME.replace(/(?<=patch_)\d*/, pos_adj);
                    fs.renameSync(MOD_FILE, path.join(path.dirname(MOD_FILE), FILE_NAME_NEW));
                }
            );
        }
    );
    return;
  }
  catch(err) {
    log('error', 'Unable to rename .patch0 files', err);
    return;
  }
}

I know the code above does not work since I end up logging the error.

Am I on the right track with this, or is there a better way of accomplishing this? If I am on the right track, could you look at the code used in the renaming function and help me troubleshoot why it doesn't work? I wasn't able to find any examples of other Vortex extensions that do a function like this for renaming individual mod files based on a load order.

As always, your guidance and assistance is greatly appreciated.

index.zip

IDCs commented 6 days ago

Hey @ChemGuy1611, due to Vortex's dependency system and how the deployment methods keep track of their file, there's currently no simple way to rename modFiles and ensure that Vortex can deploy/purge files correctly.

I'm afraid the only way I can think of getting this done, would be to use the registerMerge functor, to scan the relevant load order files. And rename it within the merged mod it creates in the staging folder.

Have the original mod files deploy to an interim location which is not loaded by the game; then have the merge call loop through those files, renaming them as per their LO index, and have the merged mod deployed where the game loads the files from.

/**
   * register a file merge that needs to happen during deployment.
   * modType is the mod type this applies to, so only mods from this mod type are merged
   * and the output merge is of that type as well.
   *
   * This api is - complex - as it tries to cover multiple related use cases. Please
   * make sure you understand how it works becauses trial&error might drive you mad.
   *
   * The way this works is that as part of deployment the "in" files get copied to a working
   * directory. It's ok for these files to be non-existent. It's ok for these files to be from one
   * of the deployed mods (see below) or a file generated by or shipped with the game itself.
   * Then the "merge" function is called on each matching file from each mod so you get an
   * opportunity to incorporate the modded content into the file in the working directory
   * Finally, the merged file from the working directory is deployed, just like every other file,
   * based on which mod type you specified.
   * If the "in" file was from mone of the mods, the merge function will be called with that
   * file again, so it's your own responsibility to not duplicate the content from that file.
   * If the "in" file did not exist, you get an empty file as the basis to merge into, that is not
   * an error.
   * The "out" path specified by the baseFiles is the relative path of the "temporary" file
   * in the working directory. Together with the mod type, this will control what the final output
   * path is.
   */
  registerMerge: (test: MergeTest, merge: MergeFunc, modType: string) => void;

  export interface IMergeFilter {
    // files to use as basis for merge, will be copied to the merge
    // directory during deployment (from in (absolute) to out (relative to working directory)
    baseFiles: (deployedFiles: IDeployedFile[]) => Array<{ in: string, out: string }>;
    // filter function, needs to match all files (relative paths) in the mod to consider
    // for merging
    filter: (fileName: string) => boolean;
  }

  /**
   * callback to determine if a merge function applies to a game. If so, return an
   * object that describes what files to merge, otherwise return undefined
   */
  export type MergeTest = (game: IGame, gameDiscovery: IDiscoveryResult) => IMergeFilter;
  /**
   * callback to do the actual merging
   */
  export type MergeFunc = (filePath: string, mergePath: string) => Promise<void>;

P.S. - note that the merge functions will get called for each mod that contains the modType you specify in an incremental manner according to the deployment order. i.e. mod x with index 5 may get called to be merged before mod y with index 1. You can't rely on the order in which you're merging the files.

IDCs commented 6 days ago

If you're looking for usage examples, give the D2: Ressurected extension or the bundled dragon-age game extensions a read. W3 and Starfield use mergers as well.