Jump to content

TES5Edit Scripting compiling woes (enumeration type, et al.)


Recommended Posts

I'm having a hard time getting a script together that will both compile in Delphi's Starter IDE and run in FO4Edit. It's typically one or the other, due to enums. For example, I can operate on DefType or ElementType returns, and have created a script that runs correctly, but it raises compile errors in IDE, which makes it difficult to check other syntax errors I may be making while trying to learn the language. That's a general problem I'm having. (But maybe I can add 'wbDefinitionsFO4' from source to uses to compile, then remove? Idk.)

 

Now I have a script which doesn't (yet) utilize those type returns and which does compile correctly but raises "Class declaration expected but '(' found", due to my apparently otherwise seemingly correct implementation of my own enumeration type, based off Delphi's online and TES5Edit's source examples. The TES5 Scripting Functions Wiki notes that the 'type' keyword is unsupported but it can be used to "alias a class name" (whatever that means). I don't know how to define this enumeration otherwise, or if I'm even allowed to use it within the confines of the API. (It's all magic and a bunch of := and ; but not += to me. Yeesh.)

 

Anyway, I'm trying to write a script that will write a collection of records to json(s). In order to do that, I first want to remove the numbering from things like 'Value #1' and otherwise put elements that are described as arrays (in 'wbDefinitionsFO4') into arrays (because ElementType doesn't tell the whole story in that regard). This entails pre-compiling lists (or delimited strings) of Names for each record type to search against as I cycle through the container(s) in order to format the json correctly. To do the search, I thought I'd grab the signature of the record and run it through a case statement, so I know which particular strings to search for. Can't do that for strings, and I don't want to write a bunch of if else statements if I can help it. So I figured I'd change to signature to its unique integer and run it through a case of similarly pre-determined signature enum integers and fork from there. Smart? Idk.(A timeit test in Python showed that, even with conversion cost, 'int in [ints]' was 2x+ faster than 'string in [strings]'. Python doesn't have cases, and from the Wiki, Edits scripts don't support 'in'. Yeesh.) I can't get my script to run in Editor, let alone set up some bench comparisons. (It did work before I added the enum/case stuff, and output in same format as an xDump, since my Write functions are adapted directly from it.)

 

Rant over. Code with snipped enum below. Any insight/guidance would be appreciated. Thanks!

unit myEditScript;

interface

uses xEditAPI, Classes, SysUtils, StrUtils, Windows, Math;

type
  TSig = (
    _AACT = 65656784,
    _NPC_ = 78806795
    );
// Global variables
var List : TStringList;
var aIndent : string;

procedure WriteElement(e : IInterface; aIndent: string = '');
procedure WriteContainer(e : IInterface; aIndent: string = '');

function RemoveNumbering(str : string): string;
function SigToTSig(SigStr: string): TSig;
function SigSwitch(Sig: TSig): integer;

implementation

function RemoveNumbering(str : string): string;
var i : integer;
begin
  Result := '';
  i := Pos(' #', str);
  if (i = 0) then 
    Result := str
  else
    Result := copy(str, 1, i-1);
end;

//idk if this works yet, different than my Python test function
function SigToTSig(SigStr: string): TSig;
var
  i, ch, e, Sig : integer;
begin
  e := 3;
  for i := 1 to 4 do
    ch := ord(SigStr[i]);
    Sig := Sig + Round(ch*IntPower(100,e));
    e := e - 1;
  Result := TSig(Sig);
end;

function SigSwitch(Sig: TSig): integer;
begin
  Result :=0;
  Case Sig of
    _NPC_: Result:=1;
  End;
end;

procedure WriteContainer(e : IInterface; aIndent: string = '');
var
  i            : Integer;
begin
  for i := 0 to Pred(ElementCount(e)) do
    WriteElement(ElementByIndex(e, i), aIndent);
end;

procedure WriteElement(e : IInterface; aIndent: string = '');
var
  Name        : string;
  Value       : string;
  Str         : string;
  defTypeStr  : string;
  eleTypeStr  : string;
  SigStr      : string;
begin

  Name := ShortName(e);
  Value := GetEditValue(e);
  SigStr := Signature(e);
  //first if just a test to see if it will catch
  if (SigSwitch(SigToTSig(SigStr)) = 1) then
    List.Add('Match');
  if (Name <> 'Unused') then begin
    if (Name <> '')then
      Str := aIndent + RemoveNumbering(Name);
    if (Name <> '') or (Value <> '') then
      aIndent := aIndent + '  ';
    if (Value <> '') then begin
      Str := Str +': '  + Value;
      List.Add(Str)
    end else begin
      if (Name <> '') then
        List.Add(Str)
    end;
  end;
  WriteContainer(e, aIndent);

end;
// Called when the script starts
function Initialize : integer;
begin
	List := TStringList.Create;
end;

// Called for each selected record in the TES5Edit tree
// If an entire plugin is selected then all records in the plugin will be processed
function Process(e : IInterface) : integer;
begin
  aIndent := '';
  WriteContainer(e, aIndent);
end;

// Called after the script has finished processing every record
function Finalize : integer;
var filename : string;
begin
	filename := ProgramPath + 'Edit Scripts\Test.txt';
	AddMessage('Saving to ' + filename);
	List.SaveToFile(filename);
	List.Free;
end;

end.
Link to comment
Share on other sites

Removed TSig type entirely. Compiles, but execution in Editor now raises 'Array type required' when attempting to get a character from a string (i.e. ch := SigStr in SigtoInt function). Idk how to get a char otherwise.

unit myEditScript;

interface

uses xEditAPI, Classes, SysUtils, StrUtils, Windows;

// Global variables
var List : TStringList;
var aIndent : string;
var Sig : integer;

procedure WriteElement(e : IInterface; Sig : integer; aIndent: string = '');
procedure WriteContainer(e : IInterface; Sig : integer; aIndent: string = '');

function RemoveNumbering(str : string): string;
function SigToInt(SigStr: string): integer;
function SigSwitch(Sig: integer): integer;

implementation

function RemoveNumbering(str : string): string;
var i : integer;
begin
  Result := '';
  i := Pos(' #', str);
  if (i = 0) then
    Result := str
  else
    Result := copy(str, 1, i-1);
end;

function SigToInt(SigStr: string): integer;
var
  i, e, Sig : integer;
  c        : string;
  ch       : Char;
begin
  e := 3;
  for i := 1 to 4 do
    ch := SigStr[i];
    Case e of
      0: Sig := Sig + ord(ch);
      1: Sig := Sig + ord(ch)*100;
      2: Sig := Sig + ord(ch)*10000;
      3: Sig := Sig + ord(ch)*1000000;
    End;
    e := e - 1;
  Result := Sig;
end;

function SigSwitch(Sig: integer): integer;
begin
  Result :=0;
  Case Sig of
    78806795:
    begin
      Result := 1;
      List.Add('NPC_ found');
    end;
  End;
end;

procedure WriteContainer(e : IInterface; Sig : integer; aIndent: string = '');
var
  i            : Integer;
begin
  for i := 0 to Pred(ElementCount(e)) do
    WriteElement(ElementByIndex(e, i), Sig, aIndent);
end;

procedure WriteElement(e : IInterface; Sig : integer; aIndent: string = '');
var
  Name        : string;
  Value       : string;
  Str         : string;
  defTypeStr  : string;
  eleTypeStr  : string;
  SigStr      : string;
begin

  Name := ShortName(e);
  Value := GetEditValue(e);
  SigSwitch(Sig);
  if (Name <> 'Unused') then begin
    if (Name <> '')then
      Str := aIndent + RemoveNumbering(Name);
    if (Name <> '') or (Value <> '') then
      aIndent := aIndent + '  ';
    if (Value <> '') then begin
      Str := Str +': '  + Value;
      List.Add(Str)
    end else begin
      if (Name <> '') then
        List.Add(Str)
    end;
  end;
  WriteContainer(e, Sig, aIndent);

end;
// Called when the script starts
function Initialize : integer;
begin
	List := TStringList.Create;
end;

// Called for each selected record in the TES5Edit tree
// If an entire plugin is selected then all records in the plugin will be processed
function Process(e : IInterface) : integer;
begin
  aIndent := '';
  Sig := SigToInt(Signature(e));
  WriteContainer(e, Sig, aIndent);
end;

// Called after the script has finished processing every record
function Finalize : integer;
var filename : string;
begin
	filename := ProgramPath + 'Edit Scripts\Test.txt';
	AddMessage('Saving to ' + filename);
	List.SaveToFile(filename);
	List.Free;
end;

end.


Edited by boozehoundblue
Link to comment
Share on other sites

True, I just thought an integer case would ultimately be more performant than comparing strings, as there's hundreds of unique signatures defined. (I also thought I'd have to nest those else ifs).

 

Still, why can't I get a char from string? From my understanding of the Wiki and JvInterpreter, this functionality was made possible in 1.51.2(?)

Edited by boozehoundblue
Link to comment
Share on other sites

xEdit uses scripting interpreter with pascal syntax, it has limited capabilties and quite slow no matter what you do. Your issue is caused by how it deals internally with types and storing all variables in Variant type. Signature() returns not a string, but an array of 4 chars. Interperter has some problems when passing types around in functions, so

SigToInt(Siganture(e))

doesn't work even though function's papameter is declared as a string, but manually assigning to a string does work because it explicitely converts Variant array value into a string

var s: string;

s := Signature(e);
SigToInt(s);

And in real Pascal you will simply do

var s: array [0..3] of AnsiChar;

s := Signature(e);
case PCardinal(@s)^ of
  ...
end;

without any char conversions because both signature and integer/cardinal are 4 bytes. But as i said this is not a real Pascal and you don't need to use such constructs in scripts.

 

I don't even understand the purpose of "case" in your script, is it some kind of a logging or statistics gathering?

Link to comment
Share on other sites

Thanks for clearing that up! Got it working. According to the Wiki, Signature(e) is defined as returning a string.

 

As far as the Case statement's purpose, I intend it as a means to operate on records individually. No, it doesn't serve a particular purpose in my posted script, but I plan to incorporate field exclusions from an ini to ultimately produce json (and other) files for different record types. The generic Write procedure(s) were mostly for me to have something to study.

 

Since both 'Case int of' and 'if SigStr = 'ABCD' accomplish the same task, the choice comes down to which looks/performs better. Maybe the performance difference isn't worth the hassle or is negated by the cost of conversion.

 

I see what you mean about being quite slow now, having run a working version over Fallout4.esm's Npcs. Yikes.

 

 

 

 

 

 

 

Link to comment
Share on other sites

The purpose of scripting in xEdit is to automate manual editing tasks, so it is always magnitudes faster than doing by hands even with relative slowness of scripts. If you need real performance, then get the free Delphi Starter edition and compile from the source code of xEdit.

Link to comment
Share on other sites

Yes, I've compiled a modified xDump.exe. The advantage of an xEdit script in my case is the visual aspect and ability to arbitrarily select records. If nothing else, perhaps I can use an xEdit script to create an ini file the xDump can utilize.

 

Thanks again for the pointers.

Link to comment
Share on other sites

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...