Jump to content
zilav

[WIPz] TES5Edit

Recommended Posts

Some users of my mod Inventory Management System Rebuilt (IMS) are reporting a " Load Order FileID [01] can not be mapped to file FileID " error.  However, the xEdit patch script I supplied with my mod does not produce such an error with the load order that I have.  After some discussion it was concluded that Complete Crafting Overhaul Remade (CCOR) was a common mod.  I downloaded CCOR and gave my patch script a try in xEdit.  I received the following error specifically:

[00:03] Exception in unit TheCraftPatch line 92: Load order FileID [01] can not be mapped to file FileID for file "IMS_Patch.esp"
[00:03] Error during Applying script "IMS - Compatibility Patch": Load order FileID [01] can not be mapped to file FileID for file "IMS_Patch.esp"

In looking at what was put into the patch plugin, I was able to deduce that once the patch script hits a record supplied by CCOR but injected into Update.esm, it craps out.

 

My question is twofold.  One, is this a bug with xEdit that needs resolved?  Two, can my patch script be modified to properly handle such records?

Patch script follows:

Spoiler

{
*************************************
**** Inventory Management System ****
*********************************************
**** IMS - Crafting Components Patch.pas
**** IMS - Recipe Item Patch.txt
**** IMS - Workstation Patch.txt
****************************************************************************************************
**** This script is a compatibility patch for Inventory Management System. 
****
**** This patch populates Form Lists with recipe components based upon their workbench keyword. 
**** Keyword and Form List pairs can be found in 'Edit Scripts\IMS - Recipe Item Patch.txt'
**** If Hearthfires.esm is active the Sawn Log item is automatically excluded.
****
**** This patch populates Form Lists with workstations by their assigned workbench keyword
**** Keyword and Form List pairs can be found in 'Edit Scripts\IMS - Workstation Patch.txt'
****
**** This patch populates Form Lists with items by their vendor keywords
**** Keyword and Form List pairs can be found in 'Edit Scripts\IMS - Vendor Items Patch.txt'
****
**** Feel free to share any local additions to the above text files.
**** Current support:
**** Base game and all DLC
**** Legacy of the Dragonborn
****
**** This patch populates Form Lists with ammo and bolt records
**** Bolts must have 'bolt' in the name in order to be recognized.
****************************************************************************************************
**** Credits:
**** Zilav
**** - wrote the initial version of this script and directed me in additional modifications
****************************************************************************************************
}
unit TheCraftPatch;

const
  sModMaster = 'Inventory Management System.esp'; // master file name
  sModPatch  = 'IMS_Patch.esp'; // patch file name

var
  slCraftIndex, slFurnIndex, slVendorIndex: TStringList;
  ModMaster, ModPatch: IInterface;
  

procedure AddObjectToList(list: string; comp: IInterface);
var
  g, flst, entries, entry: IInterface;
  i: integer;
begin
	// exclude Hearthfires Sawn Log
	If EditorID(comp) = 'BYOHMaterialLog' then 
		Exit;
		
  // create new patch plugin if doesn't exist yet
  if not Assigned(ModPatch) then begin
    ModPatch := AddNewFileName(sModPatch);
    // if failed for some reason
    if not Assigned(ModPatch) then
      raise Exception.Create('Error creating a new patch plugin!');
  end;
  
  // getting list record from patch file
  g := GroupBySignature(ModPatch, 'FLST');
  flst := MainRecordByEditorID(g, list);
  // copy as override from master if doesn't exist yet
  if not Assigned(flst) then begin
    g := GroupBySignature(ModMaster, 'FLST');
    flst := MainRecordByEditorID(g, list);
    if not Assigned(flst) then
      raise Exception.Create(list + ' FLST doesn''t exist in the master file!');
    // add masters to patch plugin before copying record
    AddRequiredElementMasters(flst, ModPatch, False);
    // copy as override into patch
    flst := wbCopyElementToFile(flst, ModPatch, False, True);
  end;
  
  entries := ElementByName(flst, 'FormIDs');
  if not Assigned(entries) then
    entries := Add(flst, 'FormIDs', True);

  for i := Pred(ElementCount(entries)) downto 0 do begin
    entry := ElementByIndex(entries, i);
    // component already in list
    if GetLoadOrderFormID(LinksTo(entry)) = GetLoadOrderFormID(comp) then
      Exit;
    // we checked all items in list but still no match found, add component to list
    if i = 0 then begin
      // if the first entry is not NULL then add a new one
      if Assigned(LinksTo(entry)) then
        entry := ElementAssign(entries, HighInteger, nil, False);
      AddMasterIfMissing(ModPatch, GetFileName(comp));
      SetEditValue(entry, Name(comp));
    end;
  end;
end;
  
procedure ProcessRecipe(e: IInterface);
var
  i: integer;
  items, item, comp: IInterface;
  kwd: string;
begin
  // workbench keyword EditorID
  kwd := EditorID(LinksTo(ElementBySignature(e, 'BNAM')));
  
  // skip workbenches with unknown keyword
  if slCraftIndex.IndexOfName(kwd) = -1 then
    Exit;
  
  // iterate over components
  items := ElementByName(e, 'Items');
  for i := 0 to Pred(ElementCount(items)) do begin
    item := ElementByIndex(items, i);
    comp := LinksTo(ElementByPath(item, 'CNTO\Item'));
    if Assigned(comp) then
      AddObjectToList(slCraftIndex.Values[kwd], comp);
  end;
end;

procedure ProcessFurniture(e: IInterface);
var
  i: integer;
  keywords: IInterface;
  kwd: string;
begin
  // skip furniture without a name, probably unplayable
  if not ElementExists(e, 'FULL') then
    Exit;
  
  // iterate over keywords
  keywords := ElementBySignature(e, 'KWDA');
  for i := 0 to Pred(ElementCount(keywords)) do begin
    kwd := EditorID(LinksTo(ElementByIndex(keywords, i)));
    // add to list and exit upon the first matching keyword
    if slFurnIndex.IndexOfName(kwd) <> -1 then begin
      AddObjectToList(slFurnIndex.Values[kwd], e);
      Exit;
    end;
  end;
end;

procedure ProcessItems(e: IInterface);
var
  i: integer;
  keywords: IInterface;
  kwd, x, fn: string;
begin
  // skip those without a name, probably unplayable
  if not ElementExists(e, 'FULL') then
    Exit;

  // iterate over keywords
  keywords := ElementBySignature(e, 'KWDA');
	fn := GetElementEditValues(e, 'FULL');
  for i := 0 to Pred(ElementCount(keywords)) do begin
		x := i;
    kwd := EditorID(LinksTo(ElementByIndex(keywords, i)));
    // add to list and exit upon the first matching keyword
    if slVendorIndex.IndexOfName(kwd) <> -1 then begin
      AddObjectToList(slVendorIndex.Values[kwd], e);
			end;
  end;
end;

procedure ProcessAmmo(e: IInterface);
var
  i: integer;
  keywords: IInterface;
  kwd, list: string;
begin
	// skip main quest arrow
  if EditorID(e) = 'MQ101SteelArrow' then
    Exit;
		
	// skip test bolts
	If EditorID(e) = 'TestDLC1Bolt' then
		Exit;

  // skip non-playable by flag or missing name
  if (GetElementNativeValues(e, 'DATA\Flags') and 2 <> 0) or not ElementExists(e, 'FULL') then
    Exit;
  
  // skip bound projectiles (checking for WeapTypeBoundArrow [KYWD:0010D501])
  keywords := ElementBySignature(e, 'KWDA');
  for i := 0 to Pred(ElementCount(keywords)) do begin
    kwd := EditorID(LinksTo(ElementByIndex(keywords, i)));
    if kwd = 'WeapTypeBoundArrow' then
      Exit;
  end;
  
  // skip practice arrows (damage=0)
  if GetElementNativeValues(e, 'DATA\Damage') = 0 then
    Exit;
  
  // determine arrow or bolt by 'bolt' word in the name
  if Pos('bolt', LowerCase(GetElementEditValues(e, 'FULL'))) <> 0 then
    list := 'abim_IMS_TotalBoltList'
  else
    list := 'abim_IMS_TotalArrowList';
    
  AddObjectToList(list, e);
end;
	
function Initialize: integer;
var
  i, j: integer;
  f, g, r: IInterface;
begin
  // locating master file and optionally patch file if exists
  for i := 0 to Pred(FileCount) do begin
    f := FileByIndex(i);
    if SameText(GetFileName(f), sModMaster) then
      ModMaster := f
    else if SameText(GetFileName(f), sModPatch) then
      ModPatch := f;
  end;

  // can't do anything without master
  if not Assigned(ModMaster) then begin
    MessageDlg(sModMaster + ' must be loaded in TES5Edit', mtInformation, [mbOk], 0);
    Result := 1;
    Exit;
  end;
	
  // associations between Workbench Keyword and list's EditorID to put components in
  slCraftIndex := TStringList.Create;
	slCraftIndex.LoadFromFile(ProgramPath + 'Edit Scripts\IMS - Recipe Item Patch.txt');
	for i := Pred(slCraftIndex.Count) downto 0 do
		if Copy(slCraftIndex[i], 1, 2) = '//' then slCraftIndex.Delete(i);
	
  //slCraftIndex.Add('CraftingCookpot=SmithingList');
  //slCraftIndex.Add('CraftingSmelter=SmithingList');
  //slCraftIndex.Add('CraftingSmithingArmorTable=SmithingList');
  //slCraftIndex.Add('CraftingSmithingForge=SmithingList');
  //slCraftIndex.Add('CraftingSmithingSharpeningWheel=SmithingList');
  //slCraftIndex.Add('CraftingSmithingSkyforge=SmithingList');
  //slCraftIndex.Add('CraftingTanningRack=TanningList');

  // associations between Workbench Keyword and list's EditorID to put furniture in
  slFurnIndex := TStringList.Create;
	slFurnIndex.LoadFromFile(ProgramPath + 'Edit Scripts\IMS - Workstation Patch.txt');
	for i := Pred(slFurnIndex.Count) downto 0 do
		if Copy(slFurnIndex[i], 1, 2) = '//' then slFurnIndex.Delete(i);

  //slFurnIndex.Add('CraftingCookpot=FurnitureCookingList');
  //slFurnIndex.Add('CraftingSmithingArmorTable=FurnitureSmithingList');

	// associations between vendor keywords and list's EditorID to put items in
	slVendorIndex := TStringList.Create;
	slVendorIndex.LoadFromFile(ProgramPath + 'Edit Scripts\IMS - Vendor Items Patch.txt');
	for i := Pred(slVendorIndex.Count) downto 0 do
		if Copy(slVendorIndex[i], 1, 2) = '//' then slVendorIndex.Delete(i);
	
	//slVendorIndex.Add('VendorItemFood=abim_IMS_VendorItemFood');
	
	
  // processing all files in load order
  for i := 0 to Pred(FileCount) do begin
    f := FileByIndex(i);
    AddMessage('Processing ' + GetFileName(f) + '...');

    // COBJ records
    g := GroupBySignature(f, 'COBJ');
    if Assigned(g) then
      for j := 0 to Pred(ElementCount(g)) do begin
        r := ElementByIndex(g, j);
        if IsMaster(r) then
          ProcessRecipe(WinningOverride(r));
      end;
    
    // FURN records
    g := GroupBySignature(f, 'FURN');
    if Assigned(g) then
      for j := 0 to Pred(ElementCount(g)) do begin
        r := ElementByIndex(g, j);
        if IsMaster(r) then
          ProcessFurniture(WinningOverride(r));
      end;

    // AMMO records
    g := GroupBySignature(f, 'AMMO');
    if Assigned(g) then
      for j := 0 to Pred(ElementCount(g)) do begin
        r := ElementByIndex(g, j);
        if IsMaster(r) then
          ProcessAmmo(WinningOverride(r));
          ProcessItems(WinningOverride(r));
      end;

		// ALCH records
		g := GroupBySignature(f, 'ALCH');
		If Assigned(g) then
			for j := 0 to Pred(ElementCount(g)) do begin
        r := ElementByIndex(g, j);
        if IsMaster(r) then
          ProcessItems(WinningOverride(r));
      end;

		// ARMO records
		g := GroupBySignature(f, 'ARMO');
		If Assigned(g) then
			for j := 0 to Pred(ElementCount(g)) do begin
        r := ElementByIndex(g, j);
        if IsMaster(r) then
          ProcessItems(WinningOverride(r));
      end;

		// BOOK records
		g := GroupBySignature(f, 'BOOK');
		If Assigned(g) then
			for j := 0 to Pred(ElementCount(g)) do begin
        r := ElementByIndex(g, j);
        if IsMaster(r) then
          ProcessItems(WinningOverride(r));
      end;

		// INGR records
		g := GroupBySignature(f, 'INGR');
		If Assigned(g) then
			for j := 0 to Pred(ElementCount(g)) do begin
        r := ElementByIndex(g, j);
        if IsMaster(r) then
          ProcessItems(WinningOverride(r));
      end;

		// KEYM records
		g := GroupBySignature(f, 'KEYM');
		If Assigned(g) then
			for j := 0 to Pred(ElementCount(g)) do begin
        r := ElementByIndex(g, j);
        if IsMaster(r) then
          ProcessItems(WinningOverride(r));
      end;

		// MISC records
		g := GroupBySignature(f, 'MISC');
		If Assigned(g) then
			for j := 0 to Pred(ElementCount(g)) do begin
        r := ElementByIndex(g, j);
        if IsMaster(r) then
          ProcessItems(WinningOverride(r));
      end;

		// SCRL records
		g := GroupBySignature(f, 'SCRL');
		If Assigned(g) then
			for j := 0 to Pred(ElementCount(g)) do begin
        r := ElementByIndex(g, j);
        if IsMaster(r) then
          ProcessItems(WinningOverride(r));
      end;

		// SLGM records
		g := GroupBySignature(f, 'SLGM');
		If Assigned(g) then
			for j := 0 to Pred(ElementCount(g)) do begin
        r := ElementByIndex(g, j);
        if IsMaster(r) then
          ProcessItems(WinningOverride(r));
      end;

		// WEAP records
		g := GroupBySignature(f, 'WEAP');
		If Assigned(g) then
			for j := 0 to Pred(ElementCount(g)) do begin
        r := ElementByIndex(g, j);
        if IsMaster(r) then
          ProcessItems(WinningOverride(r));
      end;
	end;
end;  

function Finalize: integer;
begin
  slCraftIndex.Free;
  slFurnIndex.Free;
  if Assigned(ModPatch) then begin
    CleanMasters(ModPatch);
    SortMasters(ModPatch);
  end;
end;

end.

 

 

Share this post


Link to post
Share on other sites

Skip processing injected records, they serve as masterless intercommunication between plugins

if IsInjected(record) then ...

Or you need to add as masters both the plugin injected record comes from (if it uses any links to that plugin which is usually the case for injected records) and plugin it is injected into.

Share this post


Link to post
Share on other sites
12 hours ago, zilav said:

Skip processing injected records, they serve as masterless intercommunication between plugins

if IsInjected(record) then ...

Or you need to add as masters both the plugin injected record comes from (if it uses any links to that plugin which is usually the case for injected records) and plugin it is injected into.

Thank you.

I added this to the top of the AddObjectToList procedure (right below begin) and it allowed the patch script to complete without any issue.

	// skip injected records
	If isInjected(comp) then
		Exit;

The end result seems to be okay.

Share this post


Link to post
Share on other sites

I'm interested in writing a script to merge cell and worldspace object placement edits from an esp over to a master.  How hard would this be?  Are there any TESxEdit (TES4Edit for me) script writing resources available?  Any insight for me would be nice...

Share this post


Link to post
Share on other sites

Just the amount of time it would take.  I'm working with plugins that have thousands of edits that I'm trying to merge.  I've done some manually, and it's quite tedious.  I was just hoping that a script could automate it and get it done in seconds versus the days/weeks it would take to do manually.

Share this post


Link to post
Share on other sites

It takes a few seconds and clicks to merge one plugin into the other in xEdit version 4.x, I described how to earlier in the thread. Unless you need some specific method of merging data in the records instead of complete overwriting, script will be vastly slower.

Share this post


Link to post
Share on other sites

Oh.  That's news to me.  Forgive my ignorance.  Can you give me an idea where in this 84 page (!) thread I could find this info?  I don't mind scanning some pages, but a starting point would be great.  This is going to save me a shitload of time.  Thanks.

Share this post


Link to post
Share on other sites
On 10/2/2018 at 7:52 AM, zilav said:

No, you have been asking for how to copy as override records from esp1 to esp2 without adding esp1 as a master. I told you it is impossible and you should copy and then remove master the proper way, and described how to do so. You didn't like the answer and continued to insist on adding an option to xEdit of copying records without adding masters because you've been doing it wrong for 5 years.

If you really want to know how to properly merge mods manually, then this is covered in the xEdit Training Manual (the Help button in the right top corner, and linked on every xEdit's description page on Nexus, but who reads nowadays). The latest xEdit builds made it easier:

  1. Right click on B, click Add Masters and chose A
  2. Right click on B and select "Inject Forms Into Master..." menu, chose A. It will ask to optionally preserve ObjectIDs (the YYYYYY part in FormIDs XXYYYYYY numbers), in your case it doesn't matter when simply merging two mods.
  3. Expand B, select all record groups, right click, hold Ctrl+Shift and click "Deep copy as override into", chose A. Say Yes to overwrite any changes if asked.
  4. Close and save A only. Now you have A+B properly merged into A.

Am I correct to assume this is the described method for merging two mods quickly?  Didn't take 5 minutes to track down when I decided to try...

EDIT:

The way you described above does not work in practice.  Everything is good up until step 3.  After step 2, you enter the next FormID and it automatically goes to injecting new records into mod "A".  I don't want to inject new records at this time.  I just want to merge two existing records.  I want to quickly merge worldspace object placement records.  I'll keep at it today, but I may need some assistance, if it can be done at all.

EDIT 2:

Okay, it works.  Sorry for not testing more before posting.  I guess my next question is is there a way to only merge existing records quickly (en-masse, so to speak)?  I just want to overwrite A's records with B's.  Only worldspace object placement records.  I'll RTTFM too.

EDIT 3:

Just so people know, in order to overwrite identical FormIDs, for step 3 you need to select "Deep copy as override into (with overwriting)"

Share this post


Link to post
Share on other sites

Oh, okay.  I think that is a good idea.  Learning more hotkeys that have no other means of accomplishing the same objective can be a task.  I would encourage the developers to whittle down as many hotkeys as possible.

Do you contribute to xEdit development, zilav?  Or are you just proficient in it's use?

Share this post


Link to post
Share on other sites

Quick question to those in the know: if I want to change one FormID of a record to something new (load order corrected) for merging into another mod, how do I tell what FormIDs are free in the mod I am merging into??  I'm merging into a dense mod, so there are a lot of used FormIDs.  If I have two or more FormIDs selected to change together, it's easy; just select "Change FormID" and it's taken care of by Tes4Edit.  But if I only have 1 FormID to change, it asks what I want to change it to, and in some cases I don't know because I don't know what's free.

Thanks go out to whomever answers this inquiry.

Share this post


Link to post
Share on other sites
7 hours ago, Malonn said:

Quick question to those in the know: if I want to change one FormID of a record to something new (load order corrected) for merging into another mod, how do I tell what FormIDs are free in the mod I am merging into??  I'm merging into a dense mod, so there are a lot of used FormIDs.  If I have two or more FormIDs selected to change together, it's easy; just select "Change FormID" and it's taken care of by Tes4Edit.  But if I only have 1 FormID to change, it asks what I want to change it to, and in some cases I don't know because I don't know what's free.

Thanks go out to whomever answers this inquiry.

Create any new record in a plugin, copy it's FormID, remove record.

Share this post


Link to post
Share on other sites

I've been doing similar.  For lone records, I've been using "Copy as new record into..." to create a new record for the plugin I'm merging into, then deleting it from the plugin being merged from.

But, I never thought of your way, so thanks for the answer.  "Copy as new record into..." takes fewer steps, so I'll stick with that for now.

Share this post


Link to post
Share on other sites

Does anyone know if the "NIF - Batch textures replacement" script accepts wildcards? I am trying to replace a range of different texture paths in a number of Oblivion models with one specific texture. Something like this:

  • Replace: textures\old\wood*.dds
  • With: textures\new\wood.dds

Can this be done with that script? If not, does anyone know of a tool that can help me out?

Share this post


Link to post
Share on other sites

No, it doesn't accept wildcards. You'll have to modify it for that or just provide all the possible replacement pairs (it supports any number of them). For example replace all "textures\old\wood" with something else in report only mode so it will list all such used textures in the messages, then use that info for actual replacement. I doubt it will take you more than a few minutes.

By the way it is a part of standalone Sniff tool https://www.nexusmods.com/newvegas/mods/67829

Share this post


Link to post
Share on other sites

Thanks Zilav, your Sniff tool worked like a charm. I just converted all of my models into JSONs, used Notpad++ to search and replace all the texture paths I needed then converted them back into NIFs. There were a few hundred texture paths I needed to replace and so creating a replacement pair for all of them would have probably taken a while.

Share this post


Link to post
Share on other sites

An interesting looking tool. Any chance the JSON conversion could be made to work on SSE nifs?

Share this post


Link to post
Share on other sites
12 hours ago, Arthmoor said:

An interesting looking tool. Any chance the JSON conversion could be made to work on SSE nifs?

JSON convertion works with all games from Morrowind to Fallout 4. When you select an operation in Sniff it shows the list of supported games.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Support us on Patreon!

Patreon
×
×
  • Create New...