{ ESMify Plugins 1.0 by Eddoursul https://eddoursul.win For proper overriding of temporary records, "Always save ONAM" must be enabled in xEdit. This script does not process bi-directional location references. Resave plugin in CK to rebuild them. Running it on a plugin will set the ESM flag as well. Running it on a node will only attempt to fix broken persistence. Normally, NPCs must have assigned Persistent Location. If they don't, their packages will break after conversion to ESM. Persistent Location can't be reliably set by a script, so instead we scan NPC attributes and packages, try to determine if they potentially need persistence, and set the global Persistent flag. This is a band-aid! To fix them properly, assign valid locations to actors via the Persistent Location field. } unit ESMify_Plugins; var refsChecked, recordsCounted, flaggedCount, persLocSkipped: integer; pluginAnnounced: boolean; function IsReferencedByNonLocation(rec: IwbMainRecord): boolean; var masterRecord, referencingRecord: IwbMainRecord; i: integer; sig: string; begin Result := False; masterRecord := MasterOrSelf(rec); for i := 0 to Pred(ReferencedByCount(masterRecord)) do begin referencingRecord := ReferencedByIndex(masterRecord, i); sig := Signature(referencingRecord); // Locational links are bi-directional, this script does not process them. To fix them, resave plugin in CK. // WRLD records may refer to REFRs if they are large references. This does not imply necessity of the persistence flag. // TES4 may refer to REFRs if they are listed in ONAM. To keep ONAM up to date, enable "Always save ONAM" in xEdit. if (sig = 'LCTN') or (sig = 'WRLD') or (sig = 'TES4') then continue; // Do not consider unused formlist a reference if (sig = 'FLST') and (ReferencedByCount(referencingRecord) = 0) then continue; // Only check plugins higher in the load order. // This will include non-masters as well, and will not take references from plugins lower in the load order into consideration. if GetLoadOrder(GetFile(referencingRecord)) <= GetLoadOrder(GetFile(rec)) then begin // When referencing record is not referenced and it's in the same cell, the ref does not need persistence. // Does not cover long chains of linked refs. if (ReferencedByCount(referencingRecord) = 0) then begin if (sig = 'REFR') then begin if Equals(LinksTo(ElementByPath(rec, 'Cell')), LinksTo(ElementByPath(referencingRecord, 'Cell'))) then begin continue; end; end; end; // Refs referencing themselves do not require the flag if not Equals(rec, referencingRecord) then begin result := True; break; end; end; end; end; function MarkPersistent(e: IwbMainRecord): boolean; begin AddMessage(' + Marking as persistent: ' + GetElementEditValues(e, 'NAME') + ' - (' + Name(e) + ')'); Inc(flaggedCount); SetIsPersistent(e, True); CheckNonPersistentOverride(e); end; function CheckNonPersistentOverride(e: IwbMainRecord): integer; begin if IsWinningOverride(e) then exit; if not GetIsPersistent(WinningOverride(e)) then AddMessage(' ! WARNING: the highest override of ' + ShortName(e) + ' is not persistent: ' + PathName(WinningOverride(e))); end; function GetLinkedRefNull(rec: IwbMainRecord): IwbMainRecord; var linkedRefs, currentItem: IwbElement; linkedCount, i: integer; begin linkedRefs := ElementByPath(rec, 'Linked References'); linkedCount := ElementCount(linkedRefs); for i := 0 to Pred(linkedCount) do begin currentItem := ElementByIndex(linkedRefs, i); if not Assigned(LinksTo(ElementByPath(currentItem, 'Keyword/Ref'))) then begin result := LinksTo(ElementByPath(currentItem, 'Ref')); break; end; end; end; function GetLinkedRef(rec: IwbMainRecord; keyword: IwbMainRecord): IwbMainRecord; var linkedRefs, currentItem: IwbElement; linkedCount, i: integer; begin linkedRefs := ElementByPath(rec, 'Linked References'); linkedCount := ElementCount(linkedRefs); for i := 0 to Pred(linkedCount) do begin currentItem := ElementByIndex(linkedRefs, i); if Equals(LinksTo(ElementByPath(currentItem, 'Keyword/Ref')), keyword) then begin result := LinksTo(ElementByPath(currentItem, 'Ref')); break; end; end; end; function Process(e: IInterface): integer; var currentPlugin: IwbFile; baseRefRecord, package, refCell, actorLocation, linkedRef, linkedRefKeyword: IwbMainRecord; packages: IwbElement; i, baseID, packageCount, typeId: integer; sig, baseSig, packageLoc, packageType: string; isREFR, isACHR, skip: boolean; begin if not pluginAnnounced then begin currentPlugin := GetFile(e); AddMessage(#13#10 + '* Processing ' + Name(currentPlugin)); pluginAnnounced := true; refsChecked := 0; recordsCounted := 0; flaggedCount := 0; persLocSkipped := 0; if not GetIsESM(currentPlugin) then setIsESM(currentPlugin, true); end; if (GetLoadOrderFormID(e) = 0) then begin AddMessage(#13#10 + ' ' + IntToStr(recordsCounted) + ' records scanned.'); AddMessage(' ' + IntToStr(refsChecked) + ' references checked.'); AddMessage(' ' + IntToStr(persLocSkipped) + ' actors with Persistent Location skipped.'); AddMessage(' ' + IntToStr(flaggedCount) + ' references have been marked as persistent.' + #13#10); pluginAnnounced := false; exit; end; Inc(recordsCounted); sig := Signature(e); isREFR := (sig = 'REFR'); isACHR := (sig = 'ACHR'); if (not isREFR) and (not isACHR) and (sig <> 'PHZD') then exit; Inc(refsChecked); if GetIsDeleted(e) then begin if GetIsPersistent(e) then begin AddMessage(' Removing persistence flag from deleted record: ' + ShortName(e)); SetIsPersistent(e, False); end; exit; end; baseRefRecord := BaseRecord(e); if not Assigned(baseRefRecord) then begin AddMessage(' INVALID RECORD: missing base form: ' + FullPath(e)); exit; end; if GetIsPersistent(e) then begin CheckNonPersistentOverride(e); exit; end; if isREFR then begin // Water, texture sets, and markers always get flagged by CK baseID := FormID(baseRefRecord); if (baseID = $3B) or (baseID = $4) or (baseID = $34) or (baseID = $1F) or (baseID = $15) or (baseID = $12) or (baseID = $10) or (baseID = $6) or (baseID = $5) or (baseID = $138C0) or (baseID = $3DF55) then begin MarkPersistent(e); exit; end; baseSig := Signature(baseRefRecord); if (baseSig = 'TXST') or ((baseSig = 'ACTI') and ElementExists(baseRefRecord, 'WNAM')) then begin MarkPersistent(e); exit; end; end; // Flag all records with non-locational references if IsReferencedByNonLocation(e) then begin MarkPersistent(e); exit; end; if not isACHR then exit; // This NPC uses Persistent Location, should work fine if the location was assigned correctly if ElementExists(e, 'XLCN') then begin actorLocation := LinksTo(ElementByPath(e, 'XLCN')); if ReferencedByCount(actorLocation) > 1 then begin Inc(persLocSkipped); //AddMessage(' Skipping actor with Persistent Location: ' + GetElementEditValues(e, 'NAME') + ' - (' + Name(e) + ')'); exit; end; end; // Skip Starts Dead NPCs if GetElementNativeValues(e, 'Record Header\Record Flags\Starts Dead') <> 0 then exit; // Skip simple actors (chicken and such) if GetElementNativeValues(baseRefRecord, 'ACBS\Flags\Simple Actor') <> 0 then exit; packages := ElementByPath(baseRefRecord, 'Packages'); packageCount := ElementCount(packages); // Skip actors without packages if packageCount = 0 then exit; skip := false; for i := 0 to Pred(packageCount) do begin package := LinksTo(ElementByIndex(packages, i)); if not Assigned(package) then begin skip := true; AddMessage(' ! WARNING: Invalid package entry: ' + GetElementEditValues(e, 'NAME') + ' - (' + Name(e) + ')'); continue; end; packageLoc := GetElementEditValues(ElementByIndex(ElementByPath(package, 'Package Data\Data Input Values'), 0), 'PLDT\Type'); if (packageLoc = 'Near editor location') or (packageLoc = 'Near self') then begin AddMessage(' Skipping editor location actor: ' + GetElementEditValues(e, 'NAME') + ' - (' + Name(e) + ')'); skip := true; continue; end; if (packageLoc = 'In cell') then begin refCell := LinksTo(ElementByPath(ElementByIndex(ElementByPath(package, 'Package Data\Data Input Values'), 0), 'PLDT\Cell')); if Assigned(refCell) then begin if Equals(refCell, LinksTo(ElementByPath(e, 'Cell'))) then begin AddMessage(' Skipping actor, staying in one cell: ' + GetElementEditValues(e, 'NAME') + ' - (' + Name(e) + ')'); skip := true; continue; end; end; end; if (GetElementEditValues(ElementByIndex(ElementByPath(package, 'Package Data\Data Input Values'), 0), 'PTDA\Target Data\Type') = 'Linked Reference') then begin linkedRefKeyword := LinksTo(ElementByPath(ElementByIndex(ElementByPath(package, 'Package Data\Data Input Values'), 0), 'PTDA\Target Data\Reference')); if Assigned(linkedRefKeyword) then begin linkedRef := GetLinkedRef(e, linkedRefKeyword); end else begin linkedRef := GetLinkedRefNull(e); end; if Assigned(linkedRef) then begin if Equals(LinksTo(ElementByPath(linkedRef, 'Cell')), LinksTo(ElementByPath(e, 'Cell'))) then begin AddMessage(' Skipping actor, staying in one cell with his linked ref: ' + GetElementEditValues(e, 'NAME') + ' - (' + Name(e) + ')'); skip := true; continue; end; end; end; skip := false; end; if skip then exit; MarkPersistent(e); end; function Finalize: integer; begin AddMessage('All done.'); end; end.