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.
2023-10-24 09:18:12 +00:00
Normally, NPCs must have assigned Persistent Location. If they don' t, their packages may break after conversion to ESM.
2022-10-08 14:58:22 +00:00
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 ;
2023-10-24 13:06:49 +00:00
hardcodedStatForms: TStringList;
dragonCZMarker, dragonLZMarker: IwbMainRecord;
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.
2023-10-24 09:18:12 +00:00
// WRLD records may refer to REFRs if they are large references. Being a large reference does not imply necessity of the persistence flag.
2022-10-08 14:58:22 +00:00
// 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;
// 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
2022-10-10 21:51:58 +00:00
if not SameRecord( rec, referencingRecord) then begin
2022-10-08 14:58:22 +00:00
result : = True ;
break;
end ;
end ;
end ;
end ;
2022-10-10 21:51:58 +00:00
function SameRecord( ref: IwbMainRecord; otherRef: IwbMainRecord) : boolean ;
begin
// 'Equals' does not fit here
result : = Assigned( ref) and ( GetLoadOrderFormID( ref) = GetLoadOrderFormID( otherRef) ) ;
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
2022-10-10 21:51:58 +00:00
result : = SameRecord( cell, otherCell) ;
2022-10-10 18:54:53 +00:00
exit;
end ;
worldspace : = LinksTo( ElementByPath( cell, 'Worldspace' ) ) ;
otherWorldspace : = LinksTo( ElementByPath( otherCell, 'Worldspace' ) ) ;
2022-10-10 21:51:58 +00:00
if not SameRecord( worldspace, otherWorldspace) then begin
2022-10-10 18:54:53 +00:00
result : = false ;
exit;
end ;
// Consider small world a cell
if ( GetElementNativeValues( worldspace, 'DATA' ) and 1 ) > 0 then begin
result : = true ;
exit;
end ;
2022-10-10 21:51:58 +00:00
2022-10-10 18:54:53 +00:00
// 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) ) ;
2022-10-10 21:51:58 +00:00
//AddMessage(IntToStr(coords.x) + ' ' + IntToStr(otherCoords.x));
//AddMessage(IntToStr(coords.y) + ' ' + IntToStr(otherCoords.y));
2022-10-10 18:54:53 +00:00
result : = ( coords. x = otherCoords. x) and ( coords. y = otherCoords. y) ;
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) ;
2022-10-10 23:56:16 +00:00
SetIsPersistent( e, True ) ;
2022-10-08 14:58:22 +00:00
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) ;
2022-10-10 21:51:58 +00:00
if SameRecord( LinksTo( ElementByPath( currentItem, 'Keyword/Ref' ) ) , keyword) then begin
2022-10-10 01:17:25 +00:00
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 ;
2023-10-24 13:06:49 +00:00
function IsSameCellPackage( package : IwbMainRecord) : boolean ;
var
packageLoc, packageType: string ;
refCell, linkedRefKeyword, linkedRef: IwbMainRecord;
begin
packageLoc : = GetElementEditValues( ElementByIndex( ElementByPath( package , 'Package Data\Data Input Values' ) , 0 ) , 'PLDT\Type' ) ;
if packageLoc < > '' then begin
if ( packageLoc = 'Near editor location' ) or ( packageLoc = 'Near self' ) then begin
//AddMessage(' Skipping editor location actor: ' + GetElementEditValues(e, 'NAME') + ' - (' + Name(e) + ')');
continue;
end
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 SameRecord( refCell, LinksTo( ElementByPath( e, 'Cell' ) ) ) then continue;
end ;
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;
end ;
end
else begin
packageLoc : = GetElementEditValues( ElementByIndex( ElementByPath( package , 'Package Data\Data Input Values' ) , 0 ) , 'PTDA\Target Data\Type' ) ;
if ( packageLoc = '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;
end
else if ( packageLoc = 'Specific Reference' ) then begin
linkedRef : = LinksTo( ElementByPath( ElementByIndex( ElementByPath( package , 'Package Data\Data Input Values' ) , 0 ) , 'PTDA\Target Data\Reference' ) ) ;
if InSameCell( e, linkedRef) then continue;
end ;
end ;
end ;
2023-10-24 08:46:19 +00:00
function Initialize: integer ;
2023-10-24 13:06:49 +00:00
var dobj: IwbMainRecord;
2023-10-24 08:46:19 +00:00
begin
2023-10-24 13:06:49 +00:00
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, I will update this to retrieve actual values.
hardcodedStatForms. Add( $138C0 ) ; // DragonMarker
hardcodedStatForms. Add( $3DF55 ) ; // DragonMarkerCrashStrip
2023-10-24 08:46:19 +00:00
end ;
2022-10-08 14:58:22 +00:00
function Process( e: IInterface) : integer ;
var
currentPlugin: IwbFile;
2023-10-24 13:06:49 +00:00
baseRefRecord, package , actorLocation: IwbMainRecord;
2022-10-10 01:17:25 +00:00
packages: IwbElement;
i, baseID, packageCount, typeId: integer ;
2023-10-24 13:06:49 +00:00
sig, baseSig: string ;
2022-10-10 01:17:25 +00:00
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) ;
2022-10-11 11:33:03 +00:00
if GetIsDeleted( e) then exit;
2022-10-08 14:58:22 +00:00
baseRefRecord : = BaseRecord( e) ;
if not Assigned( baseRefRecord) then begin
2023-10-24 09:18:12 +00:00
AddMessage( ' INVALID RECORD: Missing base form: ' + FullPath( e) ) ;
2022-10-08 14:58:22 +00:00
exit;
end ;
if GetIsPersistent( e) then begin
CheckNonPersistentOverride( e) ;
exit;
end ;
if isREFR then begin
2023-10-24 13:06:49 +00:00
// Certain types of references are always flagged as persistent by the CK. See DavidJCobb's post:
// https://discord.com/channels/535508975626747927/535530099475480596/1129026688077013084
2022-10-08 14:58:22 +00:00
baseSig : = Signature( baseRefRecord) ;
2023-10-24 13:06:49 +00:00
if baseSig = 'TXST' then begin
// Flag texture sets
2022-10-08 14:58:22 +00:00
MarkPersistent( e) ;
exit;
2023-10-24 13:06:49 +00:00
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' ) > 0 ) 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 ;
2022-10-08 14:58:22 +00:00
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' ) ) ;
2023-10-24 13:06:49 +00:00
// CK flags refs with the PersistAll location
if FormID( actorLocation) = $216A7 then begin // PersistAll
MarkPersistent( e) ;
exit;
end ;
2022-10-08 14:58:22 +00:00
if ReferencedByCount( actorLocation) > 1 then begin
Inc( persLocSkipped) ;
2023-10-24 13:06:49 +00:00
AddMessage( ' Skipping actor with Persistent Location: ' + GetElementEditValues( e, 'NAME' ) + ' - (' + Name( e) + ')' ) ;
2022-10-08 14:58:22 +00:00
exit;
end ;
end ;
// Skip Starts Dead NPCs
if GetElementNativeValues( e, 'Record Header\Record Flags\Starts Dead' ) < > 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;
2023-10-24 13:06:49 +00:00
// Scan packages and try to determine if their range extends beyond the NPC's starting cell.
2022-10-10 18:54:53 +00:00
skip : = true ;
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
2023-10-24 13:06:49 +00:00
if ( IsSameCellPackage( package ) ) then continue;
2022-10-08 14:58:22 +00:00
2022-10-10 01:17:25 +00:00
skip : = false ;
2023-10-24 13:06:49 +00:00
break;
2022-10-08 14:58:22 +00:00
end ;
2022-10-10 18:54:53 +00:00
if skip then begin
2023-10-24 13:06:49 +00:00
AddMessage( ' Skipping actor, staying in the same cell: ' + GetElementEditValues( e, 'NAME' ) + ' - (' + Name( e) + ')' ) ;
2022-10-10 18:54:53 +00:00
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
2023-10-24 13:06:49 +00:00
hardcodedStatForms. Clear;
2022-10-08 14:58:22 +00:00
AddMessage( 'All done.' ) ;
end ;
end .