From 64aecebcba3083e4668bcac55a860137862513bc Mon Sep 17 00:00:00 2001 From: Eddoursul Date: Sat, 8 Oct 2022 16:58:22 +0200 Subject: [PATCH] ESMify Plugins 1.0 --- ESMify_Plugins.pas | 233 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 ESMify_Plugins.pas diff --git a/ESMify_Plugins.pas b/ESMify_Plugins.pas new file mode 100644 index 0000000..67e0596 --- /dev/null +++ b/ESMify_Plugins.pas @@ -0,0 +1,233 @@ +{ + 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: IInterface): boolean; +begin + AddMessage(' + Marking as persistent: ' + GetElementEditValues(e, 'NAME') + ' - (' + Name(e) + ')'); + Inc(flaggedCount); + SetIsPersistent(e, True); + CheckNonPersistentOverride(e); +end; + +function CheckNonPersistentOverride(e: IInterface): 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 Process(e: IInterface): integer; +var + currentPlugin: IwbFile; + baseRefRecord, package, refCell, actorLocation: IwbMainRecord; + i, baseID, packageCount: integer; + sig, baseSig, packageLoc: string; + isREFR, isACHR: 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 non-persistent, non-unique actors. Multi-package bandits will probably be late on their schedules. + if GetElementNativeValues(baseRefRecord, 'ACBS\Flags\Unique') = 0 then + exit; + + // 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; + + packageCount := ElementCount(ElementByPath(baseRefRecord, 'Packages')); + + // Skip unique actors without packages (probably, flagged erroneously) + if packageCount = 0 then + exit; + + if packageCount = 1 then begin + + // Skip actors, having a single package revolving around their editor location or themselves + package := LinksTo(ElementByIndex(ElementByPath(baseRefRecord, 'Packages'), 0)); + + if not Assigned(package) then + exit; + + packageLoc := GetElementEditValues(ElementByIndex(ElementByPath(package, 'Package Data\Data Input Values'), 0), 'PLDT\Type'); + + if (Pos(packageLoc, 'Near editor location|Near self') <> 0) then begin + AddMessage(' Skipping editor location actor: ' + GetElementEditValues(e, 'NAME') + ' - (' + Name(e) + ')'); + exit; + 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) + ')'); + exit; + end; + end; + end; + + end; + + MarkPersistent(e); + +end; + +function Finalize: integer; +begin + AddMessage('All done.'); +end; + +end.