xEdit 的 Pascal 脚本 - 如何检查字符串中是否存在子字符串?

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

看起来这应该很简单,但也许不是。完全披露:我无论如何都不是程序员。我只知道通过业余爱好项目混日子,Pascal 对我来说是全新的。所以,如果有什么是非常糟糕的代码,我愿意改进。不过,我可能只打算使用这个脚本一次,所以我不太关心优化,只关心可靠的操作。

我正在为 xEdit(又名 SSEEDit)编写脚本 - 大多数人使用该工具来处理不更改丰富游戏资产的 Skyrim 和 Fallout 4 模组。目标是为游戏中的每个 BOOK 对象(原版 + 原始 DLC)转储 DESC 属性 的原始文本内容。我已经走得很远了,对我在做什么有了很好的理解,但我这辈子都无法让这部分工作。

我有一个字符串黑名单,在遍历所有 BOOK 对象时应该跳过它。我已经尝试使用 pos() 几种不同的方式,但它永远不会返回零以外的任何东西。

奇怪的是,即使我在测试中可以从 pos() 中得到正确的结果,但当我 运行 我的实际脚本时,每本书都是正匹配。看起来我实际上并没有遍历黑名单中的第一个条目。

以下是我搜索黑名单条目时返回的示例:

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

这不应该从 pos('DLC1ElderScroll','DLC2dunFahlbtharzJournal02') 返回 0...但它是。

(顺便说一句 - 如果你有 Skyrim SE,你只需要 SSEEdit 来测试它。)

这是我的完整脚本:

{
  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.

official Pascal documentation之后,pos函数的正确调用是pos(SubString, SourceString)。此外,我认为索引 TStringList 的正确方法是 blacklist[i].

您可以试试这个示例代码:

uses SysUtils, Classes;

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

输出:

8

似乎 SSEEdit 的捆绑 Pascal 引擎从 1 开始与 pos() 进行字符串匹配 - 只要我更改代码以排除 1 而不是 0,它就会按预期工作。

下面是脚本的重写,设置为仅打印消息而不写入任何文件:如果您对它的工作方式感到满意,只需将 14 行更改为

  DRY_RUN = False;

让它实际将书籍内容转储到 Output 子文件夹中。

我从你连接的两个脚本中的后者开始,我认为这是你的最新版本。

主要变化:

  • 只在 Initialize 中填充 blacklist 一次,不需要对每条记录都这样做;
  • 你不需要检查你的记录是否是一本书,因为你已经在遍历 BOOK 组,所以我颠倒了逻辑,只检查编辑器 ID 是否包含任何字符串blacklist;
  • 你真的不需要 mteFunctions,所以我删除了它并使用现有逻辑将编辑 ID 中带有 SpellTome 的记录列入黑名单 - 如果你需要再次检查关键字,您可以重新启用 mteFunctions 并使用其实用程序;
  • 检查记录是否 IsMaster 然后仅在其 WinningOverride 上工作一次 - 这避免了多次处理同一条记录,以防被官方 DLC 或 mod 覆盖,参考:https://tes5edit.github.io/docs/13-Scripting-Functions.html#IwbMainRecord

小改动:

  • 在使用任何 TStringList;
  • 后调用 Free
  • 为系统单位添加 uses 子句 - 运行 xEdit 脚本实际上不需要,但如果您为 Delphi Pascal 使用 linter,则有助于捕获错误;
  • 使用 Pred 而不是 Count - 1 - 结果应该是相同的,但看起来像在 Delphi 世界中进行迭代的标准方法;
  • 在任何 begin 之前换行,以及遵循样式指南的其他类似更改: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.