{ Skyrim ESMifier 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 may 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 Skyrim_ESMifier; var refsChecked, recordsCounted, flaggedCount, persLocSkipped: integer; pluginShown: boolean; hardcodedStatForms: TStringList; dragonCZMarker, dragonLZMarker: IwbMainRecord; ///// FUNCTIONS //////////////////////////////////// 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 usually refer to large reference REFRs. Being a large reference does not imply necessity of the persistence flag. // TES4 refers 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 formlists, created by Plugins Merge, true references if sig = 'FLST' then if ReferencedByCount(referencingRecord) = 0 then if StrEndsWith(EditorID(referencingRecord), 'Forms') 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. // Not always true, needs investigating. //if (ReferencedByCount(referencingRecord) = 0) then begin // if (sig = 'REFR') then begin // if InSameCell(rec, referencingRecord) then continue; // end; //end; // Refs referencing themselves do not require the flag if not SameRecord(rec, referencingRecord) then begin result := True; break; end; end; end; end; function SameRecord(ref: IwbMainRecord; otherRef: IwbMainRecord): boolean; begin // 'Equals' does not fit here result := Assigned(ref) and (GetLoadOrderFormID(ref) = GetLoadOrderFormID(otherRef)); end; function InSameCell(ref: IwbMainRecord; otherRef: IwbMainRecord): boolean; var cell, otherCell, worldspace, otherWorldspace: IwbMainRecord; coords, otherCoords: TwbGridCell; begin cell := LinksTo(ElementByPath(ref, 'Cell')); otherCell := LinksTo(ElementByPath(otherRef, 'Cell')); if not Assigned(otherCell) then exit; // Interior cell if (GetElementNativeValues(cell, 'DATA') and 1) > 0 then begin result := SameRecord(cell, otherCell); exit; end; worldspace := LinksTo(ElementByPath(cell, 'Worldspace')); otherWorldspace := LinksTo(ElementByPath(otherCell, 'Worldspace')); if not SameRecord(worldspace, otherWorldspace) then begin result := false; exit; end; // Consider small world a cell if (GetElementNativeValues(worldspace, 'DATA') and 1) > 0 then begin result := true; exit; end; // Persistent refs are located in 0,0 so we detect their original grid position via their position coords := wbPositionToGridCell(GetPosition(ref)); otherCoords := wbPositionToGridCell(GetPosition(otherRef)); //AddMessage(IntToStr(coords.x) + ' ' + IntToStr(otherCoords.x)); //AddMessage(IntToStr(coords.y) + ' ' + IntToStr(otherCoords.y)); result := (coords.x = otherCoords.x) and (coords.y = otherCoords.y); 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 SameRecord(LinksTo(ElementByPath(currentItem, 'Keyword/Ref')), keyword) then begin result := LinksTo(ElementByPath(currentItem, 'Ref')); break; end; end; end; function IsLinkedRefRemote(e: IwbMainRecord; linkedRefKeyword: IwbMainRecord): boolean; var linkedRef: IwbMainRecord; begin result := true; if Assigned(linkedRefKeyword) then begin linkedRef := GetLinkedRef(e, linkedRefKeyword); end else begin linkedRef := GetLinkedRefNull(e); end; if Assigned(linkedRef) then begin if InSameCell(e, linkedRef) then begin //AddMessage(' Skipping actor, staying in one cell with his linked ref: ' + GetElementEditValues(e, 'NAME') + ' - (' + Name(e) + ')'); result := false; end else if not GetIsPersistent(linkedRef) then begin MarkPersistent(linkedRef); end; end else begin // Missing linked ref. Usually, package checks for its presence. Sometimes, it's just missing. The outcome is the same, this package is not doing anything. result := false; end; end; function IsSameCellPackage(e: IInterface; inputValues: IwbElement): boolean; var packageLoc: string; inputVal: IwbElement; refCell, linkedRefKeyword, linkedRef: IwbMainRecord; i: integer; begin result := True; for i := 0 to Pred(ElementCount(inputValues)) do begin inputVal := ElementByIndex(inputValues, i); packageLoc := GetElementEditValues(inputVal, 'PLDT\Type'); if packageLoc <> '' then begin if (packageLoc = 'Near editor location') or (packageLoc = 'Near self') then begin continue; end else if (packageLoc = 'In cell') then begin refCell := LinksTo(ElementByPath(inputVal, 'PLDT\Cell')); if Assigned(refCell) then begin if SameRecord(refCell, LinksTo(ElementByPath(e, 'Cell'))) then continue; end; end else if (packageLoc = 'Near linked reference') then begin linkedRefKeyword := LinksTo(ElementByPath(inputVal, 'PLDT\Keyword')); if FormID(linkedRefKeyword) = $14 then continue; if not IsLinkedRefRemote(e, linkedRefKeyword) then continue; end else if (packageLoc = 'Near reference') then begin linkedRef := LinksTo(ElementByPath(inputVal, 'PLDT\Reference')); if FormID(linkedRef) = $14 then continue; if InSameCell(e, linkedRef) then continue; end else if (packageLoc = 'Alias (location)') or (packageLoc = 'Alias (reference)') then begin // Quest aliases become persistent dynamically continue; end; end else begin packageLoc := GetElementEditValues(inputVal, 'PTDA\Target Data\Type'); if (packageLoc = 'Linked Reference') then begin linkedRefKeyword := LinksTo(ElementByPath(inputVal, 'PTDA\Target Data\Reference')); if FormID(linkedRefKeyword) = $14 then continue; if not IsLinkedRefRemote(e, linkedRefKeyword) then continue; end else if (packageLoc = 'Specific Reference') then begin linkedRef := LinksTo(ElementByPath(inputVal, 'PTDA\Target Data\Reference')); if FormID(linkedRef) = $14 then continue; if InSameCell(e, linkedRef) then continue; end else continue; end; result := False; break; end; end; // Copied from mteFunctions by Mator function StrEndsWith(s1, s2: string): boolean; var i, n1, n2: integer; begin Result := false; n1 := Length(s1); n2 := Length(s2); if n1 < n2 then exit; Result := (Copy(s1, n1 - n2 + 1, n2) = s2); end; ///// PROCESSING //////////////////////////////////// function Initialize: integer; var dobj: IwbMainRecord; begin AddMessage('ESMify Plugins is starting...'); hardcodedStatForms := TStringList.Create; hardcodedStatForms.Add($5); // DivineMarker hardcodedStatForms.Add($6); // TempleMarker hardcodedStatForms.Add($10); // MapMarker hardcodedStatForms.Add($12); // HorseMarker hardcodedStatForms.Add($15); // MultiBoundMarker hardcodedStatForms.Add($1F); // RoomMarker hardcodedStatForms.Add($34); // XMarkerHeading hardcodedStatForms.Add($3B); // XMarker // DLZM and DCZM Default Objects. // For simplicity sake, I assume they are unchanged, which is true in 99.999% cases. // If you ever find a mod, changing these objects, update this to retrieve actual values. hardcodedStatForms.Add($138C0); // DragonMarker hardcodedStatForms.Add($3DF55); // DragonMarkerCrashStrip end; function Process(e: IInterface): integer; var currentPlugin: IwbFile; baseRefRecord, package, actorLocation: IwbMainRecord; packages: IwbElement; i, j, baseID, packageCount, typeId: integer; sig, baseSig: string; isREFR, isACHR, skip: boolean; begin if not pluginShown then begin currentPlugin := GetFile(e); AddMessage(#13#10 + '# Processing ' + Name(currentPlugin)); pluginShown := 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); pluginShown := 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 exit; 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 // Certain types of references are always flagged as persistent by the CK. See DavidJCobb's post: // https://discord.com/channels/535508975626747927/535530099475480596/1129026688077013084 baseSig := Signature(baseRefRecord); if baseSig = 'TXST' then begin // Flag texture sets MarkPersistent(e); exit; end else if baseSig = 'STAT' then begin // Flag hardcoded static forms (markers) if hardcodedStatForms.indexOf(FormID(baseRefRecord)) > -1 then begin MarkPersistent(e); exit; end; end else if baseSig = 'ACTI' then begin // Flag water activators if ElementExists(baseRefRecord, 'WNAM') then begin MarkPersistent(e); exit; end; end else if baseSig = 'LIGH' then begin // Flag Never Fades lights if GetElementNativeValues(e, 'Record Header\Record Flags\Never Fades') then begin MarkPersistent(e); exit; end; end else if baseSig = 'DOOR' then begin // Flag PrisonMarker refs and any doors with teleport data if (FormID(baseRefRecord) = $4) or (ElementExists(e, 'XTEL')) then begin // PrisonMarker MarkPersistent(e); exit; end; 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')); // CK flags refs with the PersistAll location if FormID(actorLocation) = $216A7 then begin // PersistAll MarkPersistent(e); exit; end; 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; packages := ElementByPath(baseRefRecord, 'Packages'); packageCount := ElementCount(packages); // Skip actors without packages if packageCount = 0 then exit; // Scan packages and try to determine if their range extends beyond the NPC's starting cell. skip := true; for i := 0 to Pred(packageCount) do begin package := LinksTo(ElementByIndex(packages, i)); if not Assigned(package) then begin AddMessage(' ! WARNING: Invalid package entry: ' + GetElementEditValues(e, 'NAME') + ' - (' + Name(e) + ')'); continue; end; if (IsSameCellPackage(e, ElementByPath(package, 'Package Data\Data Input Values'))) then continue; skip := false; break; end; if skip then begin //AddMessage(' Skipping actor, staying in the same cell: ' + GetElementEditValues(e, 'NAME') + ' - (' + Name(e) + ')'); exit; end; MarkPersistent(e); end; function Finalize: integer; begin hardcodedStatForms.Clear; AddMessage('All done.'); end; end.