2022-10-08 14:58:22 +00:00
{
2023-10-24 18:45:35 +00:00
Skyrim ESMifier 1.0
2022-10-08 14:58:22 +00:00
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
2023-10-24 16:29:57 +00:00
///// FUNCTIONS ////////////////////////////////////
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 15:06:00 +00:00
// 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.
2022-10-08 14:58:22 +00:00
if ( sig = 'LCTN' ) or ( sig = 'WRLD' ) or ( sig = 'TES4' ) then
continue;
2023-10-24 17:52:06 +00:00
// 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;
2022-10-08 14:58:22 +00:00
// 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.
2023-10-24 18:45:35 +00:00
// Not always true, needs investigating.
//if (ReferencedByCount(referencingRecord) = 0) then begin
// if (sig = 'REFR') then begin
// if InSameCell(rec, referencingRecord) then continue;
// end;
//end;
2022-10-08 14:58:22 +00:00
// 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 16:29:57 +00:00
function IsSameCellPackage( e: IInterface; inputValues: IwbElement) : boolean ;
2023-10-24 13:06:49 +00:00
var
2023-10-24 15:06:00 +00:00
packageLoc: string ;
inputVal: IwbElement;
2023-10-24 13:06:49 +00:00
refCell, linkedRefKeyword, linkedRef: IwbMainRecord;
2023-10-24 15:06:00 +00:00
i: integer ;
2023-10-24 13:06:49 +00:00
begin
2023-10-24 15:06:00 +00:00
result : = True ;
2023-10-24 13:06:49 +00:00
2023-10-24 15:06:00 +00:00
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' ) ) ;
2023-10-24 17:20:47 +00:00
if FormID( linkedRefKeyword) = $14 then continue;
2023-10-24 15:06:00 +00:00
if not IsLinkedRefRemote( e, linkedRefKeyword) then continue;
end
else if ( packageLoc = 'Near reference' ) then begin
2023-10-24 17:20:47 +00:00
linkedRef : = LinksTo( ElementByPath( inputVal, 'PLDT\Reference' ) ) ;
if FormID( linkedRef) = $14 then continue;
if InSameCell( e, linkedRef) then continue;
2023-10-24 17:30:00 +00:00
end
2023-10-24 17:27:17 +00:00
else if ( packageLoc = 'Alias (location)' ) or ( packageLoc = 'Alias (reference)' ) then begin
2023-10-24 17:26:55 +00:00
// Quest aliases become persistent dynamically
continue;
2023-10-24 13:06:49 +00:00
end ;
2023-10-24 15:06:00 +00:00
2023-10-24 13:06:49 +00:00
end
2023-10-24 15:06:00 +00:00
else begin
packageLoc : = GetElementEditValues( inputVal, 'PTDA\Target Data\Type' ) ;
2023-10-24 13:06:49 +00:00
2023-10-24 15:06:00 +00:00
if ( packageLoc = 'Linked Reference' ) then begin
linkedRefKeyword : = LinksTo( ElementByPath( inputVal, 'PTDA\Target Data\Reference' ) ) ;
2023-10-24 17:20:47 +00:00
if FormID( linkedRefKeyword) = $14 then continue;
2023-10-24 15:06:00 +00:00
if not IsLinkedRefRemote( e, linkedRefKeyword) then continue;
end
else if ( packageLoc = 'Specific Reference' ) then begin
linkedRef : = LinksTo( ElementByPath( inputVal, 'PTDA\Target Data\Reference' ) ) ;
2023-10-24 17:20:47 +00:00
if FormID( linkedRef) = $14 then continue;
2023-10-24 15:06:00 +00:00
if InSameCell( e, linkedRef) then continue;
2023-10-24 16:29:57 +00:00
end
else continue;
2023-10-24 13:06:49 +00:00
end ;
2023-10-24 15:06:00 +00:00
result : = False ;
break;
2023-10-24 13:06:49 +00:00
end ;
end ;
2023-10-24 17:52:06 +00:00
// 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 ;
2023-10-24 16:29:57 +00:00
///// PROCESSING ////////////////////////////////////
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.
2023-10-24 15:06:00 +00:00
// If you ever find a mod, changing these objects, update this to retrieve actual values.
2023-10-24 13:06:49 +00:00
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;
2023-10-24 15:06:00 +00:00
i, j, 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
2023-10-24 16:29:57 +00:00
if ElementExists( baseRefRecord, 'WNAM' ) then begin
2023-10-24 13:06:49 +00:00
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 17:20:47 +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 16:29:57 +00:00
if ( IsSameCellPackage( e, ElementByPath( package , 'Package Data\Data Input Values' ) ) ) 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 16:29:57 +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 .