364 lines
12 KiB
ObjectPascal
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.
|