2022-10-08 14:58:22 +00:00
{
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 ;
2022-10-10 18:54:53 +00:00
pluginShown: boolean ;
2022-10-08 14:58:22 +00:00
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
2022-10-10 18:54:53 +00:00
if InSameCell( rec, referencingRecord) then continue;
2022-10-08 14:58:22 +00:00
end ;
end ;
// Refs referencing themselves do not require the flag
if not Equals( rec, referencingRecord) then begin
result : = True ;
break;
end ;
end ;
end ;
end ;
2022-10-10 18:54:53 +00:00
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 : = Equals( cell, otherCell) ;
exit;
end ;
worldspace : = LinksTo( ElementByPath( cell, 'Worldspace' ) ) ;
otherWorldspace : = LinksTo( ElementByPath( otherCell, 'Worldspace' ) ) ;
if not Equals( 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) ) ;
result : = ( coords. x = otherCoords. x) and ( coords. y = otherCoords. y) ;
end ;
function HasScripts( e: IwbMainRecord; targetScripts: string ) : boolean ;
var
scripts, currentItem: IwbElement;
linkedCount, i: integer ;
begin
if not ElementExists( e, 'VMAD' ) then exit;
scripts : = ElementByPath( e, 'VMAD\Scripts' ) ;
for i : = 0 to Pred( ElementCount( scripts) ) do begin
currentItem : = ElementByIndex( scripts, i) ;
if Pos( GetElementEditValues( currentItem, 'ScriptName' ) , targetScripts) < > 0 then begin
result : = true ;
break;
end ;
end ;
end ;
2022-10-10 01:17:25 +00:00
function MarkPersistent( e: IwbMainRecord) : boolean ;
2022-10-08 14:58:22 +00:00
begin
AddMessage( ' + Marking as persistent: ' + GetElementEditValues( e, 'NAME' ) + ' - (' + Name( e) + ')' ) ;
Inc( flaggedCount) ;
SetIsPersistent( e, True ) ;
CheckNonPersistentOverride( e) ;
end ;
2022-10-10 01:17:25 +00:00
function CheckNonPersistentOverride( e: IwbMainRecord) : integer ;
2022-10-08 14:58:22 +00:00
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 ;
2022-10-10 01:17:25 +00:00
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 ;
2022-10-10 18:54:53 +00:00
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 ;
2022-10-08 14:58:22 +00:00
function Process( e: IInterface) : integer ;
var
currentPlugin: IwbFile;
2022-10-10 01:17:25 +00:00
baseRefRecord, package , refCell, actorLocation, linkedRef, linkedRefKeyword: IwbMainRecord;
packages: IwbElement;
i, baseID, packageCount, typeId: integer ;
sig, baseSig, packageLoc, packageType: string ;
isREFR, isACHR, skip: boolean ;
2022-10-08 14:58:22 +00:00
begin
2022-10-10 18:54:53 +00:00
if not pluginShown then begin
2022-10-08 14:58:22 +00:00
currentPlugin : = GetFile( e) ;
2022-10-10 18:54:53 +00:00
AddMessage( #13 #10 + '# Processing ' + Name( currentPlugin) ) ;
pluginShown : = true ;
2022-10-08 14:58:22 +00:00
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 ) ;
2022-10-10 18:54:53 +00:00
pluginShown : = false ;
2022-10-08 14:58:22 +00:00
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
2022-10-10 18:54:53 +00:00
AddMessage( ' ! Removing persistence flag from deleted record: ' + ShortName( e) ) ;
2022-10-08 14:58:22 +00:00
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;
2022-10-10 01:17:25 +00:00
packages : = ElementByPath( baseRefRecord, 'Packages' ) ;
packageCount : = ElementCount( packages) ;
2022-10-08 14:58:22 +00:00
2022-10-10 01:17:25 +00:00
// Skip actors without packages
2022-10-08 14:58:22 +00:00
if packageCount = 0 then
exit;
2022-10-10 18:54:53 +00:00
skip : = true ;
2022-10-08 14:58:22 +00:00
2022-10-10 01:17:25 +00:00
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 ;
2022-10-08 14:58:22 +00:00
packageLoc : = GetElementEditValues( ElementByIndex( ElementByPath( package , 'Package Data\Data Input Values' ) , 0 ) , 'PLDT\Type' ) ;
2022-10-10 18:54:53 +00:00
if packageLoc < > '' then begin
2022-10-10 01:17:25 +00:00
2022-10-10 18:54:53 +00:00
if ( packageLoc = 'Near editor location' ) or ( packageLoc = 'Near self' ) then begin
//AddMessage(' Skipping editor location actor: ' + GetElementEditValues(e, 'NAME') + ' - (' + Name(e) + ')');
continue;
2022-10-10 01:17:25 +00:00
end
2022-10-10 18:54:53 +00:00
else 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 continue;
2022-10-08 14:58:22 +00:00
end ;
2022-10-10 18:54:53 +00:00
end
else if ( packageLoc = 'Near linked reference' ) then begin
linkedRefKeyword : = LinksTo( ElementByPath( ElementByIndex( ElementByPath( package , 'Package Data\Data Input Values' ) , 0 ) , 'PLDT\Keyword' ) ) ;
if not IsLinkedRefRemote( e, linkedRefKeyword) then continue;
end
else if ( packageLoc = 'Near reference' ) then begin
if InSameCell( e, LinksTo( ElementByPath( ElementByIndex( ElementByPath( package , 'Package Data\Data Input Values' ) , 0 ) , 'PLDT\Reference' ) ) ) then continue;
2022-10-08 14:58:22 +00:00
end ;
2022-10-10 18:54:53 +00:00
end
else 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 not IsLinkedRefRemote( e, linkedRefKeyword) then continue;
2022-10-08 14:58:22 +00:00
end ;
2022-10-10 01:17:25 +00:00
skip : = false ;
2022-10-08 14:58:22 +00:00
end ;
2022-10-10 18:54:53 +00:00
if skip then begin
//AddMessage(' Skipping actor, staying in one cell: ' + GetElementEditValues(e, 'NAME') + ' - (' + Name(e) + ')');
exit;
end ;
2022-10-10 01:17:25 +00:00
2022-10-08 14:58:22 +00:00
MarkPersistent( e) ;
end ;
function Finalize: integer ;
begin
AddMessage( 'All done.' ) ;
end ;
end .