esmify-plugins/ESMify_Plugins.pas

364 lines
12 KiB
ObjectPascal

{
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;
pluginShown: 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 InSameCell(rec, referencingRecord) then continue;
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 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;
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 Equals(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 Process(e: IInterface): integer;
var
currentPlugin: IwbFile;
baseRefRecord, package, refCell, actorLocation, linkedRef, linkedRefKeyword: IwbMainRecord;
packages: IwbElement;
i, baseID, packageCount, typeId: integer;
sig, baseSig, packageLoc, packageType: 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 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 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;
packages := ElementByPath(baseRefRecord, 'Packages');
packageCount := ElementCount(packages);
// Skip actors without packages
if packageCount = 0 then
exit;
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;
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 Equals(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 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;
end;
skip := false;
end;
if skip then begin
//AddMessage(' Skipping actor, staying in one cell: ' + GetElementEditValues(e, 'NAME') + ' - (' + Name(e) + ')');
exit;
end;
MarkPersistent(e);
end;
function Finalize: integer;
begin
AddMessage('All done.');
end;
end.