esmify-plugins/ESMify_Plugins.pas

458 lines
14 KiB
ObjectPascal
Raw Normal View History

2022-10-08 14:58:22 +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;
pluginShown: boolean;
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;
// 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.
// 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;
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);
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
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
// 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));
result := (coords.x = otherCoords.x) and (coords.y = otherCoords.y);
end;
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;
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;
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
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;
2023-10-24 16:29:57 +00:00
function IsSameCellPackage(e: IInterface; inputValues: IwbElement): boolean;
var
2023-10-24 15:06:00 +00:00
packageLoc: string;
inputVal: IwbElement;
refCell, linkedRefKeyword, linkedRef: IwbMainRecord;
2023-10-24 15:06:00 +00:00
i: integer;
begin
2023-10-24 15:06:00 +00:00
result := True;
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'));
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
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
else if (packageLoc = 'Alias (location)') or (packageLoc = 'Alias (reference)') then begin
// Quest aliases become persistent dynamically
continue;
end;
2023-10-24 15:06:00 +00:00
end
2023-10-24 15:06:00 +00:00
else begin
packageLoc := GetElementEditValues(inputVal, 'PTDA\Target Data\Type');
2023-10-24 15:06:00 +00:00
if (packageLoc = 'Linked Reference') then begin
linkedRefKeyword := LinksTo(ElementByPath(inputVal, 'PTDA\Target Data\Reference'));
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'));
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;
end;
2023-10-24 15:06:00 +00:00
result := False;
break;
end;
end;
// 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;
var dobj: IwbMainRecord;
2023-10-24 08:46:19 +00:00
begin
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.
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;
baseRefRecord, package, actorLocation: IwbMainRecord;
packages: IwbElement;
2023-10-24 15:06:00 +00:00
i, j, baseID, packageCount, typeId: integer;
sig, baseSig: string;
isREFR, isACHR, skip: boolean;
2022-10-08 14:58:22 +00:00
begin
if not pluginShown then begin
2022-10-08 14:58:22 +00:00
currentPlugin := GetFile(e);
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);
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 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
// 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);
if baseSig = 'TXST' then begin
// Flag texture sets
2022-10-08 14:58:22 +00:00
MarkPersistent(e);
exit;
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
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'));
// 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);
//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;
packages := ElementByPath(baseRefRecord, 'Packages');
packageCount := ElementCount(packages);
2022-10-08 14:58:22 +00:00
// Skip actors without packages
2022-10-08 14:58:22 +00:00
if packageCount = 0 then
exit;
// Scan packages and try to determine if their range extends beyond the NPC's starting cell.
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;
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
skip := false;
break;
2022-10-08 14:58:22 +00:00
end;
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) + ')');
exit;
end;
2022-10-08 14:58:22 +00:00
MarkPersistent(e);
end;
function Finalize: integer;
begin
hardcodedStatForms.Clear;
2022-10-08 14:58:22 +00:00
AddMessage('All done.');
end;
end.