Pascal scripting for xEdit - how do I check whether a substring is present in a string?

Viewed 284

It seems like this should be straightforward, but maybe it's not. Full disclosure: I am not a programmer by any stretch. I just know enough to muddle my way through hobby projects, and Pascal is brand new for me as of today. So, if anything is just really terrible code, I'm open to improvements. I'm probably only going to use this script once, though, so I'm not terribly concerned about optimization, just reliable operation.

I'm writing a script for xEdit, aka SSEEDit - the tool most people use for mucking around with Skyrim and Fallout 4 mods that don't make changes to rich game assets. The goal is to dump the raw text content of the DESC property for every BOOK object in the game (vanilla + original DLC). I've gotten pretty far and have a decent understanding of what I'm doing, but I cannot for the life of me get this part to work.

I have a blacklist of strings which should be skipped when iterating through all the BOOK objects. I've tried using pos() several different ways, and it never comes back with anything other than zero.

The weird thing is, even if I can get a correct result from pos() in testing, every book is a positive match when I run my actual script. It also doesn't seem like I'm actually iterating past the first entry in the blacklist.

Here's an example of what's returned when I search for a blacklist entry:

EDID: DLC2dunFahlbtharzJournal02
looking in DLC2dunFahlbtharzJournal02
     for DLC1ElderScroll...
skipped DLC2dunFahlbtharzJournal02

This shouldn't be returning 0 from pos('DLC1ElderScroll','DLC2dunFahlbtharzJournal02')...but it is.

(btw - If you have Skyrim SE, you just need SSEEdit to test this.)

Here's my complete script:

{
  Dump book text
  Dumps "book text" property of every vanilla book

  Source for most copypasta:
  https://github.com/AngryAndConflict/skyrim-utils/
}
unit UserScript;
    uses mteFunctions;
    // uses RegExpr;
    
    function Initialize: integer;
    var
        f, group, e: IInterface;
        i, j: integer;
        btext,fname: String;
        slItems: TStringList;
        FileInfo: File_Content;
    begin
        AddMessage('Dumping books.....');

        slItems := TStringList.Create;

        // load item and perk stringlists
        for i := 0 to FileCount - 1 do begin
            f := FileByIndex(i);

            group := GroupBySignature(f, 'BOOK');
            for j := 0 to ElementCount(group) - 1 do begin
                e := ElementByIndex(group, j);
                slItems.Add(geev(e, 'EDID'));

                // if it's a book, do things
                if (isBook(e) = True) then begin
                    // this it the raw text content of each BOOK's DESC attribute
                    btext := geev(e,'DESC');
                    
                    if (Length(btext) > 0) then begin
                    
                        fname := ProgramPath + 'Output\' + (geev(e, 'EDID')) + '.txt';

                        // AddMessage('Successfully saved!');
                        // AddMessage(#9 + fname);
                        // AddMessage(#20);

                        // AddMessage('===ERROR=== Unable to save ' + fname + '!');
                    end;
                end;
            end;
        end;

        //AddMessage(btext)
    end;
    

    // check if item is book
    // exclude blacklisted books and spell tomes
    function isBook(item: IInterface): boolean;
    var
        formID,test: String;
        blacklist: TStringList;
        ignore: Boolean;
        i,t: integer;
    begin
        Result := false;
        ignore := false;

        blacklist := TStringList.Create;

        // blacklist to exclude - these are can be FormIDs or friendly names
        blacklist.Add('DLC1ElderScroll');
        blacklist.Add('DLC1FVBook01Falmer');
        blacklist.Add('DLC1FVBook02Falmer');
        blacklist.Add('DLC1FVBook03Falmer');
        blacklist.Add('DLC1FVBook04Falmer');
        blacklist.Add('DLC2BlackBook');
        blacklist.Add('DA04ElderScroll');
        blacklist.Add('ExpSpiderCrftBook');
        blacklist.Add('QA');
        blacklist.Add('Recipe');

        formID := geev(item,'Record Header\FormID');

        t := pos(blacklist(1),'Elder');
        test := IntToStr(t);
        AddMessage(test);

        for i := 0 to (blacklist.Count - 1) do begin
            if (pos(formID, blacklist(i)) >= 0) then begin
                AddMessage('====' + formID + '====');
                AddMessage(blacklist(i));
                // AddMessage('skipped ' + formID);

                ignore := true;
                Result := false;
                break;
            end;
        end;

        // has Keyword excludes all spell tomes
        if (Signature(item) = 'BOOK') and not (hasKeyword(item,'000937A5')) and not ignore then begin
            Result := true;
        end;
    end;
end.{
  Dump book text
  Dumps "book text" property of every vanilla book

  Source for most copypasta:
  https://github.com/AngryAndConflict/skyrim-utils/
}
unit UserScript;
    uses mteFunctions;
    // uses RegExpr;
    
    function Initialize: integer;
    var
        f, group, e: IInterface;
        i, j: integer;
        btext,fname: TString;
        slItems: TStringList;
        FileInfo: File_Content;
        blacklist: TStringList;
    begin
        AddMessage('Dumping books.....');

        slItems := TStringList.Create;

        // load item and perk stringlists
        for i := 0 to FileCount - 1 do begin
            f := FileByIndex(i);

            group := GroupBySignature(f, 'BOOK');
            for j := 0 to ElementCount(group) - 1 do begin
                e := ElementByIndex(group, j);
                slItems.Add(geev(e, 'EDID'));

                // if it's a book, do things
                if (isBook(e) = True) then begin
                    // this it the raw text content of each BOOK's DESC attribute
                    btext := geev(e,'DESC');
                    
                    if (Length(btext) > 0) then begin
                    
                        fname := ProgramPath + 'Output\' + (geev(e, 'EDID')) + '.txt';

                        // AddMessage('Successfully saved!');
                        // AddMessage(#9 + fname);
                        // AddMessage(#20);

                        // AddMessage('===ERROR=== Unable to save ' + fname + '!');
                    end;
                end;

                // AddMessage('----------')
            end;
        end;
        //AddMessage(btext)
    end;
    

    // check if item is book
    // exclude blacklisted books and spell tomes
    function isBook(item: IInterface): boolean;
    var
        formID,needle,haystack,test: TString;
        blacklist: TStringList;
        ignore: Boolean;
        i,t,p: integer;
    begin
        Result := false;
        ignore := false;

        blacklist := TStringList.Create;

        // blacklist to exclude - these are can be FormIDs or friendly names
        blacklist.Add('DLC1ElderScroll');
        blacklist.Add('DLC1FVBook01Falmer');
        blacklist.Add('DLC1FVBook02Falmer');
        blacklist.Add('DLC1FVBook03Falmer');
        blacklist.Add('DLC1FVBook04Falmer');
        blacklist.Add('DLC2BlackBook');
        blacklist.Add('DA04ElderScroll');
        blacklist.Add('ExpSpiderCrftBook');
        blacklist.Add('QA');
        blacklist.Add('Recipe');

        formID := geev(item,'EDID');

        for i := 0 to (blacklist.Count - 1) do begin
            needle := blacklist[i];
            haystack := formID;
            
            // AddMessage('====' + formID + '====');

            // AddMessage('looking in ' + haystack);
            // AddMessage(#9 + ' for ' + needle + '...');
            
            t := pos(haystack, needle);
            test := IntToStr(t);

            // AddMessage(test);

            if (pos(blacklist[i], formID) >= 0) then begin
                // AddMessage(blacklist[i]);
                AddMessage('skipped ' + formID);

                ignore := true;
                break;
            end;
        end;

        // hasKeyword excludes all spell tomes
        if (not (Signature(item) = 'BOOK')) or (hasKeyword(item,'000937A5')) then begin
            ignore := true;
        end;
        
        if ignore then begin
            Result := false;
        end;

        if not ignore then begin
            Result := true;
        end;
    end;
end.
3 Answers

Below is a rewrite of your script, set to only print messages without writing any file: if you're happy with how it works, just change line 14 to

  DRY_RUN = False;

to have it actually dump the books content into the Output subfolder.

I started from the latter of the two scripts you concatenated, I assumed that to be your latest version.

Major changes:

  • populate blacklist only once inside Initialize, no need to do it for every record;
  • you don't need to check if your record is a book, because you're already iterating over the BOOK group, so I reversed the logic and only check if the editor ID contains any string in blacklist;
  • you don't really need mteFunctions, so I removed it and used the existing logic to blacklist records with SpellTome in their editor ID - if you ever need to check for keywords again, you can enable mteFunctions back and use its utilities;
  • check if a record IsMaster and then only work once on its WinningOverride - this avoids processing the same record multiple times in case is overridden by official DLCs or mods, reference: https://tes5edit.github.io/docs/13-Scripting-Functions.html#IwbMainRecord

Minor changes:

  • call Free after using any TStringList;
  • add uses clauses for system units - not actually needed to run xEdit scripts, but useful to catch errors if you use a linter for your Delphi Pascal;
  • use Pred instead of Count - 1 - the result should be the same, but looks like the standard way to do iterations in Delphi world;
  • break lines before any begin and other similar changes following the style guide at: https://docwiki.embarcadero.com/RADStudio/Sydney/en/Delphi_Statements
{
  Dump book text
  Dumps "book text" property of every book in loaded plugins

  Source for most copypasta:
  https://github.com/AngryAndConflict/skyrim-utils/
}
unit UserScript;

interface

const
  // change this to False to actually write the text files
  DRY_RUN = True;
  // change this to False if you want to continue after the first error
  STOP_ON_ERROR = True;

implementation

uses
  //mteFunctions,
  System.Classes,   // TStringList
  System.SysUtils,  // CreateDir, IntToStr
  xEditAPI;

// check if rec editor ID contains any string in blacklist
function IsBlacklisted(rec: IwbMainRecord; blacklist: TStringList): Boolean;
var
  i: Cardinal;
  edid: string;
  //needle, haystack: string;
begin
  Result := False;
  edid := EditorID(rec);

  for i := 0 to Pred(blacklist.Count) do
  begin
    //needle := blacklist[i];
    //haystack := edid;
    //AddMessage('====' + edid + '====');
    //AddMessage('looking in ' + haystack);
    //AddMessage(#9 + ' for ' + needle + '...');
    //AddMessage(' test: ' + IntToStr(Pos(needle, haystack));

    if (Pos(blacklist[i], edid) > 0) then
    begin
      Result := True;
      //AddMessage(blacklist[i]);
      AddMessage('Skipping: ' + edid);
      Break;
    end;
  end;
end;

function Initialize: Integer;
var
  i, j: Cardinal;
  f: IwbFile;
  bookGroup: IwbGroupRecord;
  book: IwbMainRecord;
  btext, outputPath, fname: string;
  blacklist, output: TStringList;
begin
  Result := 0;
  outputPath := ProgramPath + 'Output\';

  if not DRY_RUN then
  begin
    CreateDir(outputPath);
  end;

  blacklist := TStringList.Create;

  try
    // match editor IDs containing any of following
    blacklist.Add('DLC1ElderScroll');
    blacklist.Add('DLC1FVBook01Falmer');
    blacklist.Add('DLC1FVBook02Falmer');
    blacklist.Add('DLC1FVBook03Falmer');
    blacklist.Add('DLC1FVBook04Falmer');
    blacklist.Add('DLC2BlackBook');
    blacklist.Add('DA04ElderScroll');
    blacklist.Add('ExpSpiderCrftBook');
    blacklist.Add('QA');
    blacklist.Add('Recipe');
    blacklist.Add('SpellTome');

    AddMessage('Dumping books...');

    for i := 0 to Pred(FileCount) do
    begin
      f := FileByIndex(i);
      bookGroup := GroupBySignature(f, 'BOOK');

      for j := 0 to Pred(ElementCount(bookGroup)) do
      begin
        book := ElementByIndex(bookGroup, j);

        if IsMaster(book) then
        begin
          book := WinningOverride(book);

          // if it's not blacklisted, do things
          if (not IsBlacklisted(book, blacklist)) then
          begin
            // this it the raw text content of each BOOK's DESC attribute
            btext := GetElementEditValues(book, 'DESC');

            if (Length(btext) > 0) then
            begin
              fname := outputPath + GetElementEditValues(book, 'EDID') + '.txt';
              AddMessage('Outputting to: ' + fname);

              if not DRY_RUN then
              begin
                output := TStringList.Create;

                try
                  try
                    output.Add(btext);
                    output.SaveToFile(fname);
                    //AddMessage('Successfully saved!');
                    //AddMessage(#9 + fname);
                    //AddMessage(#20);
                  except
                    AddMessage('===ERROR=== Unable to save ' + fname + '!');

                    if STOP_ON_ERROR then
                    begin
                      raise;
                    end;
                  end;
                finally
                  output.Free;
                end;
              end;
            end;
          end;
        end;

        //AddMessage('----------')
      end;
    end;

    //AddMessage(btext)
  finally
    blacklist.Free;
  end;
end;

end.

Following the official Pascal documentation, the correct call of the pos function is pos(SubString, SourceString). Moreover, I believe that the proper way to index TStringList is blacklist[i].

You can try this sample code:

uses SysUtils, Classes;

var blacklist: TStringList;
begin
  blacklist := TStringList.Create;
  blacklist.Add('Black');
  blacklist.Add('White');
  WriteLn(Pos(blacklist[1], 'Robert White'));
  ReadLn;
end.

Output:

8

It would appear SSEEdit's bundled Pascal engine starts at 1 for string matching with pos() - as soon as I changed my code to exclude 1 instead of 0, it worked as expected.

Related