DEADSOFTWARE

preliminary textual map framework; DO NOT USE!
authorKetmar Dark <ketmar@ketmar.no-ip.org>
Sat, 26 Aug 2017 00:08:48 +0000 (03:08 +0300)
committerKetmar Dark <ketmar@ketmar.no-ip.org>
Sat, 26 Aug 2017 00:09:29 +0000 (03:09 +0300)
src/shared/xdynrec.pas [new file with mode: 0644]
src/shared/xparser.pas [new file with mode: 0644]

diff --git a/src/shared/xdynrec.pas b/src/shared/xdynrec.pas
new file mode 100644 (file)
index 0000000..71a95bf
--- /dev/null
@@ -0,0 +1,1732 @@
+(* Copyright (C)  DooM 2D:Forever Developers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *)
+{$INCLUDE a_modes.inc}
+unit xdynrec;
+
+interface
+
+uses
+  xparser;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+type
+  TDynMapDef = class;
+  TDynRecord = class;
+
+  // this is base type for all scalars (and arrays)
+  TDynField = class
+  public
+    type
+      TType = (TBool, TChar, TByte, TUByte, TShort, TUShort, TInt, TUInt, TString, TPoint, TSize, TList, TTrigData);
+      // TPoint: pair of Shorts
+      // TSize: pair of UShorts
+      // TList: actually, array of records
+      // TTrigData: array of bytes
+      // arrays of chars are pascal shortstrings (with counter in the first byte)
+
+    type
+      TDynFieldArray = array of TDynField;
+      TDynRecordArray = array of TDynRecord;
+
+  private
+    type
+      TEBS = (TNone, TRec, TEnum, TBitSet);
+
+  private
+    mOwner: TDynRecord;
+    mPasName: AnsiString;
+    mName: AnsiString;
+    mType: TType;
+    mIVal: Integer; // for all integer types
+    mIVal2: Integer; // for point and size
+    mSVal: AnsiString; // string; for byte and char arrays
+    mRVal: TDynRecordArray; // for list
+    mRecRef: TDynRecord; // for record
+    mRecRefOwned: Boolean; // was mRecRef created from inline definition?
+    mMaxDim: Integer; // for byte and char arrays; <0: not an array
+    mBinOfs: Integer; // offset in binary; <0 - none
+    mRecOfs: Integer; // offset in record; <0 - none
+    mSepPosSize: Boolean; // for points and sizes, use separate fields
+    mAsT: Boolean; // for points and sizes, use separate fields, names starts with `t`
+    mDefined: Boolean;
+    mHasDefault: Boolean;
+    mDefaultValueSet: Boolean;
+    mOmitDef: Boolean;
+    mInternal: Boolean;
+    // default values
+    mDefSVal: AnsiString;
+    mEBS: TEBS;
+    mEBSName: AnsiString; // name of enum, bitset or record
+    mBitSetUnique: Boolean; // bitset can contain only one value
+    mNegBool: Boolean;
+
+    // temp
+    mDefId: AnsiString;
+
+  private
+    procedure cleanup ();
+
+    procedure parse (pr: TTextParser);
+
+    procedure setIVal (v: Integer); inline;
+    procedure setSVal (const v: AnsiString); inline;
+
+    procedure fixDefaultValue ();
+    function isDefaultValue (): Boolean;
+
+  public
+    constructor Create (const aname: AnsiString; atype: TType);
+    constructor Create (pr: TTextParser);
+    destructor Destroy (); override;
+
+    class function getTypeName (t: TType): AnsiString;
+
+    function definition (): AnsiString;
+
+    function clone (): TDynField;
+
+    procedure parseValue (pr: TTextParser);
+
+    procedure writeTo (wr: TTextWriter);
+
+    // won't work for lists
+    function isSimpleEqu (fld: TDynField): Boolean;
+
+  public
+    property pasname: AnsiString read mPasName;
+    property name: AnsiString read mName;
+    property baseType: TType read mType;
+    property defined: Boolean read mDefined write mDefined;
+    property internal: Boolean read mInternal write mInternal;
+    property ival: Integer read mIVal write setIVal;
+    property sval: AnsiString read mSVal write setSVal;
+    property list: TDynRecordArray read mRVal write mRVal;
+    property maxdim: Integer read mMaxDim; // for fixed-size arrays
+    property binOfs: Integer read mBinOfs; // offset in binary; <0 - none
+    property recOfs: Integer read mRecOfs; // offset in record; <0 - none
+    property hasDefault: Boolean read mHasDefault;
+    property defsval: AnsiString read mDefSVal write mDefSVal;
+    property ebs: TEBS read mEBS write mEBS;
+    property ebsname: AnsiString read mEBSName write mEBSName; // enum/bitset name
+
+    property x: Integer read mIVal;
+    property w: Integer read mIVal;
+    property y: Integer read mIVal2;
+    property h: Integer read mIVal2;
+  end;
+
+
+  TDynRecord = class
+  private
+    mOwner: TDynMapDef;
+    mId: AnsiString;
+    mPasName: AnsiString;
+    mName: AnsiString;
+    mSize: Integer;
+    mFields: TDynField.TDynFieldArray;
+    mTrigTypes: array of AnsiString; // if this is triggerdata, we'll hold list of triggers here
+    mHeader: Boolean; // true for header record
+
+  private
+    procedure parse (pr: TTextParser); // parse definition
+
+    function findByName (const aname: AnsiString): Integer; inline;
+    function hasByName (const aname: AnsiString): Boolean; inline;
+    function getFieldByName (const aname: AnsiString): TDynField; inline;
+
+    function getIsTrigData (): Boolean; inline;
+    function getIsForTrig (const aname: AnsiString): Boolean; inline;
+
+  public
+    constructor Create ();
+    constructor Create (pr: TTextParser); // parse definition
+    destructor Destroy (); override;
+
+    function definition (): AnsiString;
+
+    function clone (): TDynRecord;
+
+    procedure parseValue (pr: TTextParser; asheader: Boolean=false);
+
+    procedure writeTo (wr: TTextWriter; putHeader: Boolean=true);
+
+  public
+    property id: AnsiString read mId; // for map parser
+    property pasname: AnsiString read mPasName;
+    property name: AnsiString read mName; // record name
+    property size: Integer read mSize; // size in bytes
+    property fields: TDynField.TDynFieldArray read mFields write mFields;
+    property has[const aname: AnsiString]: Boolean read hasByName;
+    property field[const aname: AnsiString]: TDynField read getFieldByName;
+    property isTrigData: Boolean read getIsTrigData;
+    property isForTrig[const aname: AnsiString]: Boolean read getIsForTrig;
+  end;
+
+
+  TDynEBS = class
+  private
+    mOwner: TDynMapDef;
+    mIsEnum: Boolean;
+    mName: AnsiString;
+    mIds: array of AnsiString;
+    mVals: array of Integer;
+    mMaxName: AnsiString; // MAX field
+    mMaxVal: Integer; // max value
+
+  private
+    procedure cleanup ();
+
+    procedure parse (pr: TTextParser); // parse definition
+
+    function findByName (const aname: AnsiString): Integer; inline;
+    function hasByName (const aname: AnsiString): Boolean; inline;
+    function getFieldByName (const aname: AnsiString): Integer; inline;
+
+  public
+    constructor Create (pr: TTextParser); // parse definition
+    destructor Destroy (); override;
+
+    function definition (): AnsiString;
+
+  public
+    property name: AnsiString read mName; // record name
+    property isEnum: Boolean read mIsEnum;
+    property has[const aname: AnsiString]: Boolean read hasByName;
+    property field[const aname: AnsiString]: Integer read getFieldByName;
+  end;
+
+
+  TDynMapDef = class
+  private
+    curheader: TDynRecord; // for parser
+
+  private
+    function findRecordById (const atypename, aid: AnsiString): TDynRecord;
+
+  public
+    records: array of TDynRecord; // [0] is always header
+    trigDatas: array of TDynRecord;
+    ebs: array of TDynEBS;
+
+  private
+    procedure parse (pr: TTextParser);
+
+    function getHeader (): TDynRecord; inline;
+
+  public
+    constructor Create (pr: TTextParser);
+    destructor Destroy (); override;
+
+    function findRec (const aname: AnsiString): TDynRecord;
+    function findTrigDataFor (const aname: AnsiString): TDynRecord;
+    function findEBS (const aname: AnsiString): TDynEBS;
+
+    function parseMap (pr: TTextParser): TDynRecord;
+
+  public
+    property header: TDynRecord read getHeader;
+  end;
+
+
+implementation
+
+uses
+  SysUtils;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor TDynField.Create (const aname: AnsiString; atype: TType);
+begin
+  mRVal := nil;
+  mRecRef := nil;
+  mRecRefOwned := false;
+  cleanup();
+  mName := aname;
+  mType := atype;
+end;
+
+
+constructor TDynField.Create (pr: TTextParser);
+begin
+  cleanup();
+  parse(pr);
+end;
+
+
+destructor TDynField.Destroy ();
+begin
+  cleanup();
+  inherited;
+end;
+
+
+procedure TDynField.cleanup ();
+begin
+  mName := '';
+  mType := TType.TInt;
+  mIVal := 0;
+  mIVal2 := 0;
+  mSVal := '';
+  mRVal := nil;
+  if mRecRefOwned then mRecRef.Free();
+  mRecRef := nil;
+  mRecRefOwned := false;
+  mMaxDim := -1;
+  mBinOfs := -1;
+  mRecOfs := -1;
+  mSepPosSize := false;
+  mAsT := false;
+  mHasDefault := false;
+  mDefined := false;
+  mOmitDef := false;
+  mInternal := true;
+  mDefSVal := '';
+  mEBS := TEBS.TNone;
+  mEBSName := '';
+  mBitSetUnique := false;
+  mNegBool := false;
+  mDefId := '';
+  mDefaultValueSet := false;
+end;
+
+
+function TDynField.clone (): TDynField;
+var
+  f: Integer;
+begin
+  result := TDynField.Create(mName, mType);
+  result.mOwner := mOwner;
+  result.mPasName := mPasName;
+  result.mName := mName;
+  result.mType := mType;
+  result.mIVal := mIVal;
+  result.mIVal2 := mIVal2;
+  result.mSVal := mSVal;
+  SetLength(result.mRVal, Length(mRVal));
+  for f := 0 to High(mRVal) do result.mRVal[f] := mRVal[f].clone();
+  result.mRecRefOwned := mRecRefOwned;
+  if mRecRefOwned then
+  begin
+    if (mRecRef <> nil) then result.mRecRef := mRecRef.clone();
+  end
+  else
+  begin
+    result.mRecRef := mRecRef;
+  end;
+  result.mMaxDim := mMaxDim;
+  result.mBinOfs := mBinOfs;
+  result.mRecOfs := mRecOfs;
+  result.mSepPosSize := mSepPosSize;
+  result.mAsT := mAsT;
+  result.mDefined := mDefined;
+  result.mHasDefault := mHasDefault;
+  result.mOmitDef := mOmitDef;
+  result.mInternal := mInternal;
+  result.mDefSVal := mDefSVal;
+  result.mEBS := mEBS;
+  result.mEBSName := mEBSName;
+  result.mBitSetUnique := mBitSetUnique;
+  result.mNegBool := mNegBool;
+  result.mDefId := mDefId;
+  result.mDefaultValueSet := mDefaultValueSet;
+end;
+
+
+procedure TDynField.setIVal (v: Integer); inline; begin mIVal := v; mDefined := true; end;
+procedure TDynField.setSVal (const v: AnsiString); inline; begin mSVal := v; mDefined := true; end;
+
+
+// won't work for lists
+function TDynField.isSimpleEqu (fld: TDynField): Boolean;
+begin
+  if (fld = nil) or (mType <> fld.mType) then begin result := false; exit; end;
+  case mType of
+    TType.TBool: result := ((mIVal <> 0) = (fld.mIVal <> 0));
+    TType.TChar: result := (mSVal = fld.mSVal);
+    TType.TByte,
+    TType.TUByte,
+    TType.TShort,
+    TType.TUShort,
+    TType.TInt,
+    TType.TUInt:
+      result := (mIVal = fld.mIVal);
+    TType.TString: result := (mSVal = fld.mSVal);
+    TType.TPoint,
+    TType.TSize:
+      result := ((mIVal = fld.mIVal) and (mIVal2 = fld.mIVal2));
+    TType.TList: result := false;
+    TType.TTrigData: result := false;
+    else raise Exception.Create('ketmar forgot to handle some field type');
+  end;
+end;
+
+
+procedure TDynField.fixDefaultValue ();
+var
+  stp: TTextParser;
+  s: AnsiString;
+begin
+  if not mDefined then
+  begin
+    if not mHasDefault then
+    begin
+      if mInternal then exit;
+      raise Exception.Create(Format('field ''%s'' in record ''%s'' of record type ''%s'' is not set', [mName, mOwner.mId, mOwner.mName]));
+    end;
+    if (mEBS = TEBS.TRec) then
+    begin
+      if (CompareText(mDefSVal, 'null') <> 0) then raise Exception.Create(Format('field ''%s'' in record ''%s'' of record type ''%s'' has non-null default value ''%s''', [mName, mOwner.mId, mOwner.mName, mDefSVal]));
+      mDefined := true;
+      assert(mRecRef = nil);
+      mDefaultValueSet := true;
+      exit;
+    end;
+    s := '';
+    case mType of
+      TType.TChar, TType.TString: s := TTextParser.quote(mDefSVal)+';';
+      TType.TPoint, TType.TSize: assert(false); // no default values for these types yet
+      else s := mDefSVal+';';
+    end;
+    //mDefined := true;
+    //writeln('DEFAULT for <', mName, '>: <', s, '>');
+    stp := TStrTextParser.Create(s);
+    try
+      parseValue(stp);
+    finally
+      stp.Free();
+    end;
+    assert(mDefined);
+    mDefaultValueSet := true;
+  end;
+end;
+
+
+function TDynField.isDefaultValue (): Boolean;
+var
+  fld: TDynField = nil;
+  stp: TTextParser = nil;
+  s: AnsiString;
+begin
+  if not mHasDefault then begin result := false; exit; end;
+  //result := mDefaultValueSet;
+  if (mEBS = TEBS.TRec) then begin result := (mRecRef = nil); exit; end;
+  s := '';
+  case mType of
+    TType.TChar, TType.TString: s := TTextParser.quote(mDefSVal)+';';
+    TType.TPoint, TType.TSize: begin result := false; exit; end; // no default values for these types yet
+    else s := mDefSVal+';';
+  end;
+  stp := TStrTextParser.Create(s);
+  try
+    fld := clone();
+    fld.parseValue(stp);
+    result := isSimpleEqu(fld);
+  finally
+    fld.Free();
+    stp.Free();
+  end;
+end;
+
+
+class function TDynField.getTypeName (t: TType): AnsiString;
+begin
+  case t of
+    TType.TBool: result := 'bool';
+    TType.TChar: result := 'char';
+    TType.TByte: result := 'byte';
+    TType.TUByte: result := 'ubyte';
+    TType.TShort: result := 'short';
+    TType.TUShort: result := 'ushort';
+    TType.TInt: result := 'int';
+    TType.TUInt: result := 'uint';
+    TType.TString: result := 'string';
+    TType.TPoint: result := 'point';
+    TType.TSize: result := 'size';
+    TType.TList: result := 'array';
+    TType.TTrigData: result := 'trigdata';
+    else raise Exception.Create('ketmar forgot to handle some field type');
+  end;
+end;
+
+
+function TDynField.definition (): AnsiString;
+begin
+  result := mPasName+' is '+TTextParser.quote(mName)+' type ';
+  result += getTypeName(mType);
+  if (mMaxDim >= 0) then result += Format('[%d]', [mMaxDim]);
+  if (mRecOfs >= 0) then result += Format(' offset %d', [mRecOfs]);
+  case mEBS of
+    TEBS.TNone: begin end;
+    TEBS.TRec: result += ' '+mEBSName;
+    TEBS.TEnum: result += ' enum '+mEBSName;
+    TEBS.TBitSet: begin result += ' bitset '; if mBitSetUnique then result += 'unique '; result += mEBSName; end;
+  end;
+  if mHasDefault then
+  begin
+    if (mType = TType.TChar) or (mType = TType.TString) then result += ' default '+TTextParser.quote(mDefSVal)
+    else if (Length(mDefSVal) > 0) then result += ' default '+mDefSVal;
+    {
+    else
+    begin
+      if (mType = TType.TBool) then
+      begin
+        result += ' default ';
+        if (mDefIVal <> 0) then result += 'true' else result += 'false';
+      end
+      else
+      begin
+        result += Format(' default %d', [mDefIVal]);
+      end;
+    end;
+    }
+  end;
+  if mSepPosSize then
+  begin
+         if (mType = TType.TPoint) then begin if (mAsT) then result += ' as txy' else result += ' as xy'; end
+    else if (mType = TType.TSize) then begin if (mAsT) then result += ' as twh' else result += ' as wh'; end;
+  end;
+  if mOmitDef then result += ' omitdefault';
+  if mInternal then result += ' internal';
+end;
+
+
+procedure TDynField.parse (pr: TTextParser);
+var
+  fldname: AnsiString;
+  fldtype: AnsiString;
+  fldofs: Integer;
+  fldrecname: AnsiString;
+  fldpasname: AnsiString;
+  asxy, aswh, ast: Boolean;
+  ainternal: Boolean;
+  omitdef: Boolean;
+  defstr: AnsiString;
+  defint: Integer;
+  hasdefStr: Boolean;
+  hasdefInt: Boolean;
+  hasdefId: Boolean;
+  lmaxdim: Integer;
+  lebs: TDynField.TEBS;
+  unique: Boolean;
+begin
+  fldpasname := '';
+  fldname := '';
+  fldtype := '';
+  fldofs := -1;
+  fldrecname := '';
+  asxy := false;
+  aswh := false;
+  ast := false;
+  ainternal := false;
+  omitdef := false;
+  defstr := '';
+  defint := 0;
+  hasdefStr := false;
+  hasdefInt := false;
+  hasdefId := false;
+  unique := false;
+  lmaxdim := -1;
+  lebs := TDynField.TEBS.TNone;
+
+  fldpasname := pr.expectId(); // pascal field name
+  // field name
+  pr.expectId('is');
+  fldname := pr.expectStr();
+  // field type
+  pr.expectId('type');
+  fldtype := pr.expectId();
+
+  // fixed-size array?
+  if pr.eatDelim('[') then
+  begin
+    lmaxdim := pr.expectInt();
+    if (lmaxdim < 1) then raise Exception.Create(Format('invali field ''%s'' array size', [fldname]));
+    pr.expectDelim(']');
+  end;
+
+  while (pr.tokType <> pr.TTSemi) do
+  begin
+    if pr.eatId('offset') then
+    begin
+      if (fldofs >= 0) then raise Exception.Create(Format('duplicate field ''%s'' offset', [fldname]));
+      fldofs := pr.expectInt();
+      if (fldofs < 0) then raise Exception.Create(Format('invalid field ''%s'' offset', [fldname]));
+      continue;
+    end;
+
+    if pr.eatId('as') then
+    begin
+           if pr.eatId('xy') then asxy := true
+      else if pr.eatId('wh') then aswh := true
+      else if pr.eatId('txy') then begin asxy := true; ast := true; end
+      else if pr.eatId('twh') then begin aswh := true; ast := true; end
+      else raise Exception.Create(Format('invalid field ''%s'' as what?', [fldname]));
+      continue;
+    end;
+
+    if pr.eatId('enum') then
+    begin
+      lebs := TDynField.TEBS.TEnum;
+      if (Length(fldrecname) <> 0) then raise Exception.Create(Format('field ''%s'' already typed as ''%s''', [fldname, fldrecname]));
+      fldrecname := pr.expectId();
+      continue;
+    end;
+
+    if pr.eatId('bitset') then
+    begin
+      lebs := TDynField.TEBS.TBitSet;
+      if (Length(fldrecname) <> 0) then raise Exception.Create(Format('field ''%s'' already typed as ''%s''', [fldname, fldrecname]));
+      unique := pr.eatId('unique');
+      fldrecname := pr.expectId();
+      continue;
+    end;
+
+    if pr.eatId('default') then
+    begin
+      if hasdefStr or hasdefInt or hasdefId then raise Exception.Create(Format('field ''%s'' has duplicate default', [fldname]));
+      case pr.tokType of
+        pr.TTStr:
+          begin
+            hasdefStr := true;
+            defstr := pr.expectStr(true); // allow empty strings
+          end;
+        pr.TTId:
+          begin
+            hasdefId := true;
+            defstr := pr.expectId();
+          end;
+        pr.TTInt:
+          begin
+            hasdefInt := true;
+            defint := pr.expectInt();
+          end;
+        else
+          raise Exception.Create(Format('field ''%s'' has invalid default', [fldname]));
+      end;
+      continue;
+    end;
+
+    if pr.eatId('omitdefault') then
+    begin
+      omitdef := true;
+      continue;
+    end;
+
+    if pr.eatId('internal') then
+    begin
+      ainternal := true;
+      continue;
+    end;
+
+    if (pr.tokType <> pr.TTId) then raise Exception.Create(Format('field ''%s'' has something unexpected in definition', [fldname]));
+
+    if (Length(fldrecname) <> 0) then raise Exception.Create(Format('field ''%s'' already typed as ''%s''', [fldname, fldrecname]));
+    fldrecname := pr.expectId();
+    lebs := TDynField.TEBS.TRec;
+  end;
+
+  pr.expectTT(pr.TTSemi);
+
+  // create field
+  mName := fldname;
+       if (fldtype = 'bool') then mType := TType.TBool
+  else if (fldtype = 'negbool') then begin mType := TType.TBool; mNegBool := true; end
+  else if (fldtype = 'char') then mType := TType.TChar
+  else if (fldtype = 'byte') then mType := TType.TByte
+  else if (fldtype = 'ubyte') then mType := TType.TUByte
+  else if (fldtype = 'short') then mType := TType.TShort
+  else if (fldtype = 'ushort') then mType := TType.TUShort
+  else if (fldtype = 'int') then mType := TType.TInt
+  else if (fldtype = 'uint') then mType := TType.TUInt
+  else if (fldtype = 'string') then mType := TType.TString
+  else if (fldtype = 'point') then mType := TType.TPoint
+  else if (fldtype = 'size') then mType := TType.TSize
+  else if (fldtype = 'trigdata') then mType := TType.TTrigData
+  else raise Exception.Create(Format('field ''%s'' has invalid type ''%s''', [fldname, fldtype]));
+
+  {if hasdefId and (self.baseType = self.TType.TBool) then
+  begin
+         if (defstr = 'true') or (defstr = 'tan') or (defstr = 'yes') then self.mDefIVal := 1
+    else if (defstr = 'false') or (defstr = 'ona') or (defstr = 'no') then self.mDefIVal := 0
+    else raise Exception.Create(Format('field ''%s'' has invalid boolean default ''%s''', [fldname, defstr]));
+  end
+  else}
+  begin
+         if hasdefStr then self.mDefSVal := defstr
+    else if hasdefInt then self.mDefSVal := Format('%d', [defint])
+    else if hasdefId then self.mDefSVal := defstr;
+  end;
+
+  self.mHasDefault := (hasdefStr or hasdefId or hasdefInt);
+  self.mPasName := fldpasname;
+  self.mEBS := lebs;
+  self.mEBSName := fldrecname;
+  self.mBitSetUnique := unique;
+  self.mMaxDim := lmaxdim;
+  self.mBinOfs := fldofs;
+  self.mRecOfs := fldofs;
+  self.mSepPosSize := (asxy or aswh);
+  self.mAsT := ast;
+  self.mOmitDef := omitdef;
+  self.mInternal := ainternal;
+end;
+
+
+procedure TDynField.writeTo (wr: TTextWriter);
+var
+  def: TDynMapDef;
+  es: TDynEBS = nil;
+  f, mask: Integer;
+  first, found: Boolean;
+begin
+  wr.put(mName);
+  wr.put(' ');
+  // if this field should contain struct, convert type and parse struct
+  case mEBS of
+    TEBS.TNone: begin end;
+    TEBS.TRec:
+      begin
+        if (mRecRef = nil) then
+        begin
+          wr.put('null;'#10);
+        end
+        else if mRecRefOwned then
+        begin
+          mRecRef.writeTo(wr, false); // only data, no header
+        end
+        else
+        begin
+          wr.put(mRecRef.mId);
+          wr.put(';'#10);
+        end;
+        exit;
+      end;
+    TEBS.TEnum:
+      begin
+        def := mOwner.mOwner;
+        es := def.findEBS(mEBSName);
+        if (es = nil) then raise Exception.Create(Format('record enum type ''%s'' for field ''%s'' not found', [mEBSName, mName]));
+        for f := 0 to High(es.mVals) do
+        begin
+          if (es.mVals[f] = mIVal) then
+          begin
+            wr.put(es.mIds[f]);
+            wr.put(';'#10);
+            exit;
+          end;
+        end;
+        raise Exception.Create(Format('value %d in record enum type ''%s'' for field ''%s'' not found', [mIVal, mEBSName, mName]));
+      end;
+    TEBS.TBitSet:
+      begin
+        def := mOwner.mOwner;
+        es := def.findEBS(mEBSName);
+        if (es = nil) then raise Exception.Create(Format('record bitset type ''%s'' for field ''%s'' not found', [mEBSName, mName]));
+        // none?
+        if (mIVal = 0) then
+        begin
+          for f := 0 to High(es.mVals) do
+          begin
+            if (es.mVals[f] = 0) then
+            begin
+              wr.put(es.mIds[f]);
+              wr.put(';'#10);
+              exit;
+            end;
+          end;
+          raise Exception.Create(Format('value %d in record bitset type ''%s'' for field ''%s'' not found', [0, mEBSName, mName]));
+        end;
+        // not none
+        mask := 1;
+        first := true;
+        while (mask <> 0) do
+        begin
+          if ((mIVal and mask) <> 0) then
+          begin
+            found := false;
+            for f := 0 to High(es.mVals) do
+            begin
+              if (es.mVals[f] = mask) then
+              begin
+                if not first then wr.put('+') else first := false;
+                wr.put(es.mIds[f]);
+                found := true;
+                break;
+              end;
+            end;
+            if not found then raise Exception.Create(Format('value %d in record bitset type ''%s'' for field ''%s'' not found', [mask, mEBSName, mName]));
+          end;
+          mask := mask shl 1;
+        end;
+        wr.put(';'#10);
+        exit;
+      end;
+    else raise Exception.Create('ketmar forgot to handle some EBS type');
+  end;
+
+  case mType of
+    TType.TBool:
+      begin
+        if (mIVal = 0) then wr.put('false;'#10) else wr.put('true;'#10);
+        exit;
+      end;
+    TType.TChar:
+      begin
+        if (mMaxDim = 0) then raise Exception.Create(Format('invalid string size definition for field ''%s''', [mName]));
+        wr.put(TTextParser.quote(mSVal));
+        wr.put(';'#10);
+        exit;
+      end;
+    TType.TByte,
+    TType.TUByte,
+    TType.TShort,
+    TType.TUShort,
+    TType.TInt,
+    TType.TUInt:
+      begin
+        wr.put('%d;'#10, [mIVal]);
+        exit;
+      end;
+    TType.TString:
+      begin
+        wr.put(TTextParser.quote(mSVal));
+        wr.put(';'#10);
+        exit;
+      end;
+    TType.TPoint,
+    TType.TSize:
+      begin
+        wr.put('(%d %d);'#10, [mIVal, mIVal2]);
+        exit;
+      end;
+    TType.TList:
+      begin
+        assert(false);
+        exit;
+      end;
+    TType.TTrigData:
+      begin
+        assert(false);
+        exit;
+      end;
+    else raise Exception.Create('ketmar forgot to handle some field type');
+  end;
+  raise Exception.Create(Format('cannot parse field ''%s'' yet', [mName]));
+end;
+
+
+procedure TDynField.parseValue (pr: TTextParser);
+
+  procedure parseInt (min, max: Integer);
+  begin
+    mIVal := pr.expectInt();
+    if (mIVal < min) or (mIVal > max) then raise Exception.Create(Format('invalid %s value for field ''%s''', [getTypeName(mType), mName]));
+    mDefined := true;
+  end;
+
+var
+  rec, rc: TDynRecord;
+  def: TDynMapDef;
+  es: TDynEBS = nil;
+  tfld: TDynField;
+  tk: AnsiString;
+begin
+  // if this field should contain struct, convert type and parse struct
+  case mEBS of
+    TEBS.TNone: begin end;
+    TEBS.TRec:
+      begin
+        def := mOwner.mOwner;
+        // ugly hack. sorry.
+        if (CompareText(mEBSName, 'triggerdata') = 0) then
+        begin
+          rec := mOwner;
+          // find trigger definition
+          tfld := rec.field['type'];
+          if (tfld = nil) then raise Exception.Create(Format('triggerdata value for field ''%s'' in record ''%s'' without ''type'' field', [mName, rec.mName]));
+          if (tfld.mEBS <> TEBS.TEnum) then raise Exception.Create(Format('triggerdata value for field ''%s'' in record ''%s'' with bad ''type'' field', [mName, rec.mName]));
+          rc := def.findTrigDataFor(tfld.mSVal);
+          if (rc = nil) then raise Exception.Create(Format('triggerdata definition for field ''%s'' in record ''%s'' with type ''%s'' not found', [mName, rec.mName, tfld.mSVal]));
+          rc := rc.clone();
+          rc.parseValue(pr);
+          if mRecRefOwned then mRecRef.Free();
+          mRecRefOwned := true;
+          mRecRef := rc;
+          mDefined := true;
+          exit;
+        end;
+        // other record types
+        if (pr.tokType = pr.TTId) then
+        begin
+          rec := def.findRecordById(mEBSName, pr.tokStr);
+          if (rec = nil) then raise Exception.Create(Format('record ''%s'' (%s) value for field ''%s'' not found', [pr.tokStr, mEBSName, mName]));
+          pr.expectId();
+          if mRecRefOwned then mRecRef.Free();
+          mRecRefOwned := false;
+          mRecRef := rec;
+          mDefined := true;
+          pr.expectTT(pr.TTSemi);
+          exit;
+        end
+        else if (pr.tokType = pr.TTBegin) then
+        begin
+          rec := def.findRec(mEBSName);
+          if (rec = nil) then raise Exception.Create(Format('record type ''%s'' for field ''%s'' not found', [mEBSName, mName]));
+          rc := rec.clone();
+          rc.parseValue(pr);
+          if mRecRefOwned then mRecRef.Free();
+          mRecRefOwned := true;
+          mRecRef := rc;
+          mDefined := true;
+          exit;
+        end;
+        pr.expectTT(pr.TTBegin);
+      end;
+    TEBS.TEnum:
+      begin
+        def := mOwner.mOwner;
+        es := def.findEBS(mEBSName);
+        if (es = nil) then raise Exception.Create(Format('record enum type ''%s'' for field ''%s'' not found', [mEBSName, mName]));
+        tk := pr.expectId();
+        if not es.has[tk] then raise Exception.Create(Format('record enum value ''%s'' of type ''%s'' for field ''%s'' not found', [tk, mEBSName, mName]));
+        mIVal := es.field[tk];
+        mSVal := tk;
+        //writeln('ENUM ', mEBSName, '; element <', mSVal, '> with value ', mIVal);
+        mDefined := true;
+        pr.expectTT(pr.TTSemi);
+        exit;
+      end;
+    TEBS.TBitSet:
+      begin
+        def := mOwner.mOwner;
+        es := def.findEBS(mEBSName);
+        if (es = nil) then raise Exception.Create(Format('record bitset type ''%s'' for field ''%s'' not found', [mEBSName, mName]));
+        mIVal := 0;
+        while true do
+        begin
+          tk := pr.expectId();
+          if not es.has[tk] then raise Exception.Create(Format('record bitset value ''%s'' of type ''%s'' for field ''%s'' not found', [tk, mEBSName, mName]));
+          mIVal := mIVal or es.field[tk];
+          mSVal := tk;
+          if (pr.tokType <> pr.TTDelim) or ((pr.tokChar <> '|') and (pr.tokChar <> '+')) then break;
+          if mBitSetUnique then raise Exception.Create(Format('record bitset of type ''%s'' for field ''%s'' expects only one value', [tk, mEBSName, mName]));
+          //pr.expectDelim('|');
+          pr.skipToken(); // plus or pipe
+        end;
+        mDefined := true;
+        pr.expectTT(pr.TTSemi);
+        exit;
+      end;
+    else raise Exception.Create('ketmar forgot to handle some EBS type');
+  end;
+
+  case mType of
+    TType.TBool:
+      begin
+             if pr.eatId('true') or pr.eatId('tan') or pr.eatId('yes') then mIVal := 1
+        else if pr.eatId('false') or pr.eatId('ona') or pr.eatId('no') then mIVal := 0
+        else raise Exception.Create(Format('invalid bool value for field ''%s''', [mName]));
+        mDefined := true;
+        pr.expectTT(pr.TTSemi);
+        exit;
+      end;
+    TType.TChar:
+      begin
+        if (mMaxDim = 0) then raise Exception.Create(Format('invalid string size definition for field ''%s''', [mName]));
+        mSVal := pr.expectStr(true);
+        if (mMaxDim < 0) then
+        begin
+          // single char
+          if (Length(mSVal) <> 1) then raise Exception.Create(Format('invalid string size for field ''%s''', [mName]));
+          mIVal := Integer(mSVal[1]);
+          mSVal := '';
+        end
+        else
+        begin
+          // string
+          if (Length(mSVal) > mMaxDim) then raise Exception.Create(Format('invalid string size for field ''%s''', [mName]));
+        end;
+        mDefined := true;
+        pr.expectTT(pr.TTSemi);
+        exit;
+      end;
+    TType.TByte:
+      begin
+        parseInt(-128, 127);
+        pr.expectTT(pr.TTSemi);
+        exit;
+      end;
+    TType.TUByte:
+      begin
+        parseInt(0, 255);
+        pr.expectTT(pr.TTSemi);
+        exit;
+      end;
+    TType.TShort:
+      begin
+        parseInt(-32768, 32768);
+        pr.expectTT(pr.TTSemi);
+        exit;
+      end;
+    TType.TUShort:
+      begin
+        parseInt(0, 65535);
+        pr.expectTT(pr.TTSemi);
+        exit;
+      end;
+    TType.TInt:
+      begin
+        parseInt(Integer($80000000), $7fffffff);
+        pr.expectTT(pr.TTSemi);
+        exit;
+      end;
+    TType.TUInt:
+      begin
+        parseInt(0, $7fffffff); //FIXME
+        pr.expectTT(pr.TTSemi);
+        exit;
+      end;
+    TType.TString:
+      begin
+        mSVal := pr.expectStr(true);
+        mDefined := true;
+        pr.expectTT(pr.TTSemi);
+        exit;
+      end;
+    TType.TPoint,
+    TType.TSize:
+      begin
+        pr.expectDelim('(');
+        mIVal := pr.expectInt();
+        if (mType = TType.TPoint) then
+        begin
+          if (mIVal < -32768) or (mIVal > 32767) then raise Exception.Create(Format('invalid %s value for field ''%s''', [getTypeName(mType), mName]));
+        end
+        else
+        begin
+          if (mIVal < 0) or (mIVal > 32767) then raise Exception.Create(Format('invalid %s value for field ''%s''', [getTypeName(mType), mName]));
+        end;
+        mIVal2 := pr.expectInt();
+        if (mType = TType.TPoint) then
+        begin
+          if (mIVal2 < -32768) or (mIVal2 > 32767) then raise Exception.Create(Format('invalid %s value for field ''%s''', [getTypeName(mType), mName]));
+        end
+        else
+        begin
+          if (mIVal2 < 0) or (mIVal2 > 32767) then raise Exception.Create(Format('invalid %s value for field ''%s''', [getTypeName(mType), mName]));
+        end;
+        mDefined := true;
+        pr.expectDelim(')');
+        pr.expectTT(pr.TTSemi);
+        exit;
+      end;
+    TType.TList:
+      begin
+        assert(false);
+        exit;
+      end;
+    TType.TTrigData:
+      begin
+        assert(false);
+        exit;
+      end;
+    else raise Exception.Create('ketmar forgot to handle some field type');
+  end;
+  raise Exception.Create(Format('cannot parse field ''%s'' yet', [mName]));
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor TDynRecord.Create (pr: TTextParser);
+begin
+  if (pr = nil) then raise Exception.Create('cannot create record type without type definition');
+  mId := '';
+  mName := '';
+  mSize := 0;
+  mFields := nil;
+  mTrigTypes := nil;
+  mHeader := false;
+  parse(pr);
+end;
+
+
+constructor TDynRecord.Create ();
+begin
+  mName := '';
+  mSize := 0;
+  mFields := nil;
+  mTrigTypes := nil;
+  mHeader := false;
+end;
+
+
+destructor TDynRecord.Destroy ();
+begin
+  mName := '';
+  mFields := nil;
+  mTrigTypes := nil;
+  inherited;
+end;
+
+
+function TDynRecord.findByName (const aname: AnsiString): Integer; inline;
+begin
+  result := 0;
+  while (result < Length(mFields)) do
+  begin
+    if (CompareText(aname, mFields[result].mName) = 0) then exit;
+    Inc(result);
+  end;
+  result := -1;
+end;
+
+
+function TDynRecord.hasByName (const aname: AnsiString): Boolean; inline;
+begin
+  result := (findByName(aname) >= 0);
+end;
+
+
+function TDynRecord.getFieldByName (const aname: AnsiString): TDynField; inline;
+var
+  f: Integer;
+begin
+  f := findByName(aname);
+  if (f >= 0) then result := mFields[f] else result := nil;
+end;
+
+
+function TDynRecord.getIsTrigData (): Boolean; inline;
+begin
+  result := (Length(mTrigTypes) > 0);
+end;
+
+
+function TDynRecord.getIsForTrig (const aname: AnsiString): Boolean; inline;
+var
+  f: Integer;
+begin
+  result := true;
+  for f := 0 to High(mTrigTypes) do if (CompareText(mTrigTypes[f], aname) = 0) then exit;
+  result := false;
+end;
+
+
+function TDynRecord.clone (): TDynRecord;
+var
+  f: Integer;
+begin
+  result := TDynRecord.Create();
+  result.mOwner := mOwner;
+  result.mId := mId;
+  result.mPasName := mPasName;
+  result.mName := mName;
+  result.mSize := mSize;
+  result.mHeader := mHeader;
+  SetLength(result.mFields, Length(mFields));
+  for f := 0 to High(mFields) do
+  begin
+    result.mFields[f] := mFields[f].clone();
+    result.mFields[f].mOwner := result;
+  end;
+  SetLength(result.mTrigTypes, Length(mTrigTypes));
+  for f := 0 to High(mTrigTypes) do result.mTrigTypes[f] := mTrigTypes[f];
+end;
+
+
+procedure TDynRecord.parse (pr: TTextParser);
+var
+  fld: TDynField;
+  tdn: AnsiString;
+begin
+  if pr.eatId('TriggerData') then
+  begin
+    pr.expectId('for');
+    if pr.eatDelim('(') then
+    begin
+      while true do
+      begin
+        while pr.eatTT(pr.TTComma) do begin end;
+        if pr.eatDelim(')') then break;
+        tdn := pr.expectId();
+        if isForTrig[tdn] then raise Exception.Create(Format('duplicate trigdata ''%s'' trigtype ''%s''', [mName, tdn]));
+        SetLength(mTrigTypes, Length(mTrigTypes)+1);
+        mTrigTypes[High(mTrigTypes)] := tdn;
+      end;
+    end
+    else
+    begin
+      tdn := pr.expectId();
+      SetLength(mTrigTypes, 1);
+      mTrigTypes[0] := tdn;
+    end;
+  end
+  else
+  begin
+    mPasName := pr.expectId(); // pascal record name
+    pr.expectId('is');
+    mName := pr.expectStr();
+    if pr.eatId('header') then mHeader := true;
+    if pr.eatId('size') then
+    begin
+      mSize := pr.expectInt();
+      if (mSize < 1) then raise Exception.Create(Format('invalid record ''%s'' size: %d', [mName, mSize]));
+      pr.expectId('bytes');
+    end;
+    if pr.eatId('header') then mHeader := true;
+  end;
+
+  pr.expectTT(pr.TTBegin);
+  // load fields
+  while (pr.tokType <> pr.TTEnd) do
+  begin
+    fld := TDynField.Create(pr);
+    if hasByName(fld.name) then begin fld.Free(); raise Exception.Create(Format('duplicate field ''%s''', [fld.name])); end;
+    // append
+    fld.mOwner := self;
+    SetLength(mFields, Length(mFields)+1);
+    mFields[High(mFields)] := fld;
+    // done with field
+    //writeln('DEF: ', fld.definition);
+  end;
+  pr.expectTT(pr.TTEnd);
+end;
+
+
+function TDynRecord.definition (): AnsiString;
+var
+  f: Integer;
+begin
+  if isTrigData then
+  begin
+    // trigger data
+    result := 'TriggerData for ';
+    if (Length(mTrigTypes) > 1) then
+    begin
+      result += '(';
+      for f := 0 to High(mTrigTypes) do
+      begin
+        if (f <> 0) then result += ', ';
+        result += mTrigTypes[f];
+      end;
+      result += ')';
+    end
+    else
+    begin
+      result += mTrigTypes[0];
+    end;
+  end
+  else
+  begin
+    // record
+    result := mPasName+' is '+TTextParser.quote(mName);
+    if (mSize >= 0) then result += Format(' size %d bytes', [mSize]);
+    if mHeader then result += ' header';
+  end;
+  result += ' {'#10;
+  for f := 0 to High(mFields) do
+  begin
+    result += '  ';
+    result += mFields[f].definition;
+    result += ';'#10;
+  end;
+  result += '}';
+end;
+
+
+procedure TDynRecord.writeTo (wr: TTextWriter; putHeader: Boolean=true);
+var
+  f, c: Integer;
+  fld: TDynField;
+begin
+  if putHeader then
+  begin
+    wr.put(mName);
+    if (Length(mId) > 0) then begin wr.put(' '); wr.put(mId); end;
+    wr.put(' ');
+  end;
+  wr.put('{'#10);
+  wr.indent();
+  try
+    for f := 0 to High(mFields) do
+    begin
+      fld := mFields[f];
+      // record list?
+      if (fld.mType = fld.TType.TList) then
+      begin
+        if not mHeader then raise Exception.Create('record list in non-header record');
+        for c := 0 to High(fld.mRVal) do
+        begin
+          wr.putIndent();
+          fld.mRVal[c].writeTo(wr, true);
+        end;
+        continue;
+      end;
+      if fld.mInternal then continue;
+      if fld.mOmitDef and fld.isDefaultValue then continue;
+      wr.putIndent();
+      fld.writeTo(wr);
+    end;
+  finally
+    wr.unindent();
+  end;
+  wr.putIndent();
+  wr.put('}'#10);
+end;
+
+
+procedure TDynRecord.parseValue (pr: TTextParser; asheader: Boolean=false);
+var
+  f, c: Integer;
+  fld: TDynField;
+  rec: TDynRecord;
+  success: Boolean;
+begin
+  if (mOwner = nil) then raise Exception.Create(Format('can''t parse record ''%s'' value without owner', [mName]));
+
+  if not asheader then
+  begin
+    // id?
+    if (pr.tokType = pr.TTId) then mId := pr.expectId();
+  end;
+
+  writeln('parsing record <', mName, '>');
+  pr.expectTT(pr.TTBegin);
+  while (pr.tokType <> pr.TTEnd) do
+  begin
+    if (pr.tokType <> pr.TTId) then raise Exception.Create('identifier expected');
+
+    writeln('<', pr.tokStr, ':', asheader, '>');
+
+    // records
+    if (asheader) then
+    begin
+      assert(self = mOwner.curheader);
+      success := false;
+      for f := 0 to High(mOwner.records) do
+      begin
+        if (CompareText(mOwner.records[f].mName, pr.tokStr) = 0) then
+        begin
+          // find (or create) list of records with this type
+          fld := field[pr.tokStr];
+          if (fld = nil) then
+          begin
+            // first record
+            fld := TDynField.Create(mOwner.records[f].mName, TDynField.TType.TList);
+            fld.mOwner := self;
+            SetLength(mFields, Length(mFields)+1);
+            mFields[High(mFields)] := fld;
+          end;
+          if (fld.mType <> TDynField.TType.TList) then raise Exception.Create(Format('thing ''%s'' in record ''%s'' must be record', [fld.mName, mName]));
+          rec := mOwner.records[f].clone();
+          try
+            pr.skipToken();
+            rec.parseValue(pr);
+            if (Length(rec.mId) > 0) then
+            begin
+              for c := 0 to High(fld.mRVal) do
+              begin
+                if (Length(fld.mRVal[c].mId) > 0) and (CompareText(fld.mRVal[c].mId, rec.mId) = 0) then raise Exception.Create(Format('duplicate thing ''%s'' in record ''%s''', [fld.mName, mName]));
+              end;
+            end;
+            SetLength(fld.mRVal, Length(fld.mRVal)+1);
+            fld.mRVal[High(fld.mRVal)] := rec;
+            writeln('added ''', mOwner.records[f].mName, ''' with id ''', rec.mId, ''' (total:', Length(fld.mRVal), ')');
+            //assert(mOwner.findRecordById(mOwner.records[f].mName, rec.mId) <> nil);
+            rec := nil;
+          finally
+            rec.Free();
+          end;
+          success := true;
+          break;
+        end;
+      end;
+      if success then continue;
+    end;
+
+    // fields
+    fld := field[pr.tokStr];
+    if (fld <> nil) then
+    begin
+      if fld.defined then raise Exception.Create(Format('duplicate field ''%s'' in record ''%s''', [fld.mName, mName]));
+      if fld.internal then raise Exception.Create(Format('internal field ''%s'' in record ''%s''', [fld.mName, mName]));
+      pr.skipToken();
+      fld.parseValue(pr);
+      continue;
+    end;
+
+    // something is wrong
+    raise Exception.Create(Format('unknown field ''%s'' in record ''%s''', [pr.tokStr, mName]));
+  end;
+  pr.expectTT(pr.TTEnd);
+  // fix field defaults
+  for f := 0 to High(mFields) do mFields[f].fixDefaultValue();
+  writeln('done parsing record <', mName, '>');
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor TDynEBS.Create (pr: TTextParser);
+begin
+  cleanup();
+  parse(pr);
+end;
+
+
+destructor TDynEBS.Destroy ();
+begin
+  cleanup();
+  inherited;
+end;
+
+
+procedure TDynEBS.cleanup ();
+begin
+  mIsEnum := false;
+  mName := '';
+  mIds := nil;
+  mVals := nil;
+  mMaxName := '';
+  mMaxVal := 0;
+end;
+
+
+function TDynEBS.findByName (const aname: AnsiString): Integer;
+begin
+  result := 0;
+  while (result < Length(mIds)) do
+  begin
+    if (CompareText(aname, mIds[result]) = 0) then exit;
+    Inc(result);
+  end;
+  result := -1;
+end;
+
+
+function TDynEBS.hasByName (const aname: AnsiString): Boolean; inline;
+begin
+  result := (findByName(aname) >= 0);
+end;
+
+
+function TDynEBS.getFieldByName (const aname: AnsiString): Integer; inline;
+var
+  f: Integer;
+begin
+  f := findByName(aname);
+  if (f >= 0) then result := mVals[f] else result := 0;
+end;
+
+
+function TDynEBS.definition (): AnsiString;
+var
+  f, cv: Integer;
+begin
+  if mIsEnum then result :='enum ' else result := 'bitset ';
+  result += mName;
+  result += ' {'#10;
+  // fields
+  if mIsEnum then cv := 0 else cv := 1;
+  for f := 0 to High(mIds) do
+  begin
+    if (mIds[f] = mMaxName) then continue;
+    result += '  '+mIds[f];
+    if (mVals[f] <> cv) then
+    begin
+      result += Format(' = %d', [mVals[f]]);
+      if mIsEnum then cv := mVals[f];
+      result += ','#10;
+    end
+    else
+    begin
+      result += Format(', // %d'#10, [mVals[f]]);
+    end;
+    if mIsEnum then Inc(cv) else if (mVals[f] = cv) then cv := cv shl 1;
+  end;
+  // max field
+  if (Length(mMaxName) > 0) then result += '  '+mMaxName+' = MAX,'#10;
+  result += '}';
+end;
+
+
+procedure TDynEBS.parse (pr: TTextParser);
+var
+  idname: AnsiString;
+  cv, v: Integer;
+  f: Integer;
+  skipAdd: Boolean;
+  hasV: Boolean;
+begin
+       if pr.eatId('enum') then mIsEnum := true
+  else if pr.eatId('bitset') then mIsEnum := false
+  else pr.expectId('enum');
+  mName := pr.expectId();
+  mMaxVal := Integer($80000000);
+  if mIsEnum then cv := 0 else cv := 1;
+  pr.expectTT(pr.TTBegin);
+  while (pr.tokType <> pr.TTEnd) do
+  begin
+    idname := pr.expectId();
+    for f := 0 to High(mIds) do
+    begin
+      if (CompareText(mIds[f], idname) = 0) then raise Exception.Create(Format('duplicate field ''%s'' in enum/bitset ''%s''', [idname, mName]));
+    end;
+    if (CompareText(mMaxName, idname) = 0) then raise Exception.Create(Format('duplicate field ''%s'' in enum/bitset ''%s''', [idname, mName]));
+    skipAdd := false;
+    hasV := false;
+    v := cv;
+    // has value?
+    if pr.eatDelim('=') then
+    begin
+      if pr.eatId('MAX') then
+      begin
+        if (Length(mMaxName) > 0) then raise Exception.Create(Format('duplicate max field ''%s'' in enum/bitset ''%s''', [idname, mName]));
+        mMaxName := idname;
+        skipAdd := true;
+      end
+      else
+      begin
+        v := pr.expectInt();
+          if mIsEnum then cv := v;
+        hasV := true;
+      end;
+    end;
+    // append it?
+    if not skipAdd then
+    begin
+      // fix maxvalue
+      if mIsEnum or (not hasV) then
+      begin
+        if (mMaxVal < v) then mMaxVal := v;
+      end;
+      SetLength(mIds, Length(mIds)+1);
+      mIds[High(mIds)] := idname;
+      SetLength(mVals, Length(mIds));
+      mVals[High(mVals)] := v;
+      // next cv
+      if mIsEnum or (not hasV) then
+      begin
+        if mIsEnum then Inc(cv) else cv := cv shl 1;
+      end;
+    end;
+    if (pr.tokType = pr.TTEnd) then break;
+    pr.expectTT(pr.TTComma);
+    while pr.eatTT(pr.TTComma) do begin end;
+  end;
+  pr.expectTT(pr.TTEnd);
+  // add max field
+  if (Length(mMaxName) > 0) then
+  begin
+    SetLength(mIds, Length(mIds)+1);
+    mIds[High(mIds)] := mMaxName;
+    SetLength(mVals, Length(mIds));
+    mVals[High(mVals)] := mMaxVal;
+  end;
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor TDynMapDef.Create (pr: TTextParser);
+begin
+  records := nil;
+  trigDatas := nil;
+  ebs := nil;
+  curheader := nil;
+  parse(pr);
+end;
+
+
+destructor TDynMapDef.Destroy ();
+var
+  f: Integer;
+begin
+  for f := 0 to High(records) do records[f].Free();
+  for f := 0 to High(trigDatas) do trigDatas[f].Free();
+  for f := 0 to High(ebs) do ebs[f].Free();
+  records := nil;
+  trigDatas := nil;
+  ebs := nil;
+  inherited;
+end;
+
+
+function TDynMapDef.getHeader (): TDynRecord; inline;
+begin
+  if (Length(records) = 0) then raise Exception.Create('no header in empty mapdef');
+  result := records[0];
+end;
+
+
+function TDynMapDef.findRec (const aname: AnsiString): TDynRecord;
+var
+  f: Integer;
+begin
+  for f := 0 to High(records) do
+  begin
+    if (CompareText(records[f].name, aname) = 0) then begin result := records[f]; exit; end;
+  end;
+  result := nil;
+end;
+
+
+function TDynMapDef.findTrigDataFor (const aname: AnsiString): TDynRecord;
+var
+  f: Integer;
+begin
+  for f := 0 to High(trigDatas) do
+  begin
+    if (trigDatas[f].isForTrig[aname]) then begin result := trigDatas[f]; exit; end;
+  end;
+  result := nil;
+end;
+
+
+function TDynMapDef.findEBS (const aname: AnsiString): TDynEBS;
+var
+  f: Integer;
+begin
+  for f := 0 to High(ebs) do
+  begin
+    if (CompareText(ebs[f].name, aname) = 0) then begin result := ebs[f]; exit; end;
+  end;
+  result := nil;
+end;
+
+
+function TDynMapDef.findRecordById (const atypename, aid: AnsiString): TDynRecord;
+var
+  rec: TDynRecord;
+  fld: TDynField;
+  f: Integer;
+begin
+  result := nil;
+  if (curheader = nil) then exit;
+  // find record type
+  //writeln('searching for type <', atypename, '>');
+  rec := findRec(atypename);
+  if (rec = nil) then exit;
+  // find record data
+  //writeln('searching for data of type <', atypename, '>');
+  fld := curheader.field[atypename];
+  if (fld = nil) then exit;
+  if (fld.mType <> fld.TType.TList) then exit;
+  // find by id
+  //writeln('searching for data of type <', atypename, '> with id <', aid, '> (', Length(fld.mRVal), ')');
+  for f := 0 to High(fld.mRVal) do
+  begin
+    if (CompareText(fld.mRVal[f].mId, aid) = 0) then
+    begin
+      //writeln('  FOUND!');
+      result := fld.mRVal[f];
+      exit;
+    end;
+  end;
+  // alas
+end;
+
+
+procedure TDynMapDef.parse (pr: TTextParser);
+var
+  dr, hdr: TDynRecord;
+  eb: TDynEBS;
+  f: Integer;
+begin
+  hdr := nil;
+  while true do
+  begin
+    if not pr.skipBlanks() then break;
+    if (pr.tokType <> pr.TTId) then raise Exception.Create('identifier expected');
+
+    if (pr.tokStr = 'enum') or (pr.tokStr = 'bitset') then
+    begin
+      eb := TDynEBS.Create(pr);
+      if (findEBS(eb.name) <> nil) then
+      begin
+        eb.Free();
+        raise Exception.Create(Format('duplicate enum/bitset ''%s''', [eb.name]));
+      end;
+      eb.mOwner := self;
+      SetLength(ebs, Length(ebs)+1);
+      ebs[High(ebs)] := eb;
+      //writeln(eb.definition); writeln;
+      continue;
+    end;
+
+    if (pr.tokStr = 'TriggerData') then
+    begin
+      dr := TDynRecord.Create(pr);
+      for f := 0 to High(dr.mTrigTypes) do
+      begin
+        if (findTrigDataFor(dr.mTrigTypes[f]) <> nil) then
+        begin
+          dr.Free();
+          raise Exception.Create(Format('duplicate trigdata ''%s''', [dr.mTrigTypes[f]]));
+        end;
+      end;
+      dr.mOwner := self;
+      SetLength(trigDatas, Length(trigDatas)+1);
+      trigDatas[High(trigDatas)] := dr;
+      //writeln(dr.definition); writeln;
+      continue;
+    end;
+
+    dr := TDynRecord.Create(pr);
+    //writeln(dr.definition); writeln;
+    if (findRec(dr.name) <> nil) then begin dr.Free(); raise Exception.Create(Format('duplicate record ''%s''', [dr.name])); end;
+    if (hdr <> nil) and (CompareText(dr.name, hdr.name) = 0) then begin dr.Free(); raise Exception.Create(Format('duplicate record ''%s''', [dr.name])); end;
+    dr.mOwner := self;
+    if dr.mHeader then
+    begin
+      if (hdr <> nil) then begin dr.Free(); raise Exception.Create(Format('duplicate header record ''%s'' (previous is ''%s'')', [dr.name, hdr.name])); end;
+      hdr := dr;
+    end
+    else
+    begin
+      SetLength(records, Length(records)+1);
+      records[High(records)] := dr;
+    end;
+  end;
+
+  if (hdr = nil) then raise Exception.Create('header definition not found in mapdef');
+  SetLength(records, Length(records)+1);
+  for f := High(records) downto 1 do records[f] := records[f-1];
+  records[0] := hdr;
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+function TDynMapDef.parseMap (pr: TTextParser): TDynRecord;
+var
+  res: TDynRecord = nil;
+begin
+  if (curheader <> nil) then raise Exception.Create('cannot call `parseMap()` recursively, sorry');
+  result := nil;
+  try
+    pr.expectId(header.name);
+    res := header.clone();
+    curheader := res;
+    res.parseValue(pr, true); // as header
+    result := res;
+    res := nil;
+  finally
+    curheader := nil;
+    res.Free();
+  end;
+end;
+
+
+end.
diff --git a/src/shared/xparser.pas b/src/shared/xparser.pas
new file mode 100644 (file)
index 0000000..503a9a0
--- /dev/null
@@ -0,0 +1,901 @@
+(* Copyright (C)  DooM 2D:Forever Developers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *)
+{$INCLUDE a_modes.inc}
+unit xparser;
+
+interface
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+type
+  TUtf8DecoderFast = packed record
+  public
+    const Replacement = $FFFD; // replacement char for invalid unicode
+    const Accept = 0;
+    const Reject = 12;
+
+  private
+    state: LongWord;
+
+  public
+    codepoint: LongWord; // decoded codepoint (valid only when decoder is in "complete" state)
+
+  public
+    constructor Create (v: Boolean{fuck you, fpc});
+
+    procedure reset (); inline;
+
+    function complete (): Boolean; inline; // is current character complete? take `codepoint` then
+    function invalid (): Boolean; inline;
+    function completeOrInvalid (): Boolean; inline;
+
+    // process one byte, return `true` if codepoint is ready
+    function decode (b: Byte): Boolean; inline; overload;
+    function decode (c: AnsiChar): Boolean; inline; overload;
+  end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+type
+  TTextParser = class
+  public
+    const
+      TTNone = -1;
+      TTEOF = 0;
+      TTId = 1;
+      TTInt = 2;
+      //TTFloat = 3; // not yet
+      TTStr = 4; // string
+      TTComma = 5; // ','
+      TTColon = 6; // ':'
+      TTSemi = 7; // ';'
+      TTBegin = 8; // left curly
+      TTEnd = 9; // right curly
+      TTDelim = 10; // other delimiters
+
+  private
+    mLine, mCol: Integer;
+    mCurChar, mNextChar: AnsiChar;
+
+    mAllowSignedNumbers: Boolean; // internal control
+
+    mTokLine, mTokCol: Integer; // token start
+    mTokType: Integer;
+    mTokStr: AnsiString; // string or identifier
+    mTokChar: AnsiChar; // for delimiters
+    mTokInt: Integer;
+
+  protected
+    procedure warmup (); virtual; abstract; // called in constructor to warm up the system
+    procedure loadNextChar (); virtual; abstract; // loads next char into mNextChar; #0 means 'eof'
+
+  public
+    class function quote (const s: AnsiString): AnsiString;
+
+  public
+    constructor Create (loadToken: Boolean=true);
+    destructor Destroy (); override;
+
+    function isEOF (): Boolean; inline;
+
+    function skipChar (): Boolean; // returns `false` on eof
+
+    function skipBlanks (): Boolean; // ...and comments; returns `false` on eof
+
+    function skipToken (): Boolean; // returns `false` on eof
+
+    function expectId (): AnsiString;
+    procedure expectId (const aid: AnsiString);
+    function eatId (const aid: AnsiString): Boolean;
+
+    function expectStr (allowEmpty: Boolean=false): AnsiString;
+    function expectInt (): Integer;
+
+    procedure expectTT (ttype: Integer);
+    function eatTT (ttype: Integer): Boolean;
+
+    function expectDelim (const ch: AnsiChar): AnsiChar;
+    function eatDelim (const ch: AnsiChar): Boolean;
+
+  public
+    property col: Integer read mCol;
+    property line: Integer read mLine;
+
+    property curChar: AnsiChar read mCurChar;
+    property nextChar: AnsiChar read mNextChar;
+
+    // token start
+    property tokCol: Integer read mTokCol;
+    property tokLine: Integer read mTokLine;
+
+    property tokType: Integer read mTokType; // see TTXXX constants
+    property tokStr: AnsiString read mTokStr; // string or identifier
+    property tokChar: AnsiChar read mTokChar; // for delimiters
+    property tokInt: Integer read mTokInt;
+  end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+type
+  TFileTextParser = class(TTextParser)
+  private
+    mFile: File;
+
+  protected
+    procedure warmup (); override; // called in constructor to warm up the system
+    procedure loadNextChar (); override; // loads next char into mNextChar; #0 means 'eof'
+
+  public
+    constructor Create (const fname: AnsiString; loadToken: Boolean=true);
+    destructor Destroy (); override;
+  end;
+
+  TStrTextParser = class(TTextParser)
+  private
+    mStr: AnsiString;
+    mPos: Integer;
+
+  protected
+    procedure warmup (); override; // called in constructor to warm up the system
+    procedure loadNextChar (); override; // loads next char into mNextChar; #0 means 'eof'
+
+  public
+    constructor Create (const astr: AnsiString; loadToken: Boolean=true);
+    destructor Destroy (); override;
+  end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+type
+  TTextWriter = class
+  protected
+    mIndent: Integer;
+
+  protected
+    procedure putBuf (constref buf; len: SizeUInt); virtual; abstract;
+
+  public
+    constructor Create ();
+
+    procedure put (const s: AnsiString); overload;
+    procedure put (v: Byte); overload;
+    procedure put (v: Integer); overload;
+    procedure put (const fmt: AnsiString; args: array of const); overload;
+    procedure putIndent ();
+    procedure indent ();
+    procedure unindent ();
+  end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+type
+  TFileTextWriter = class(TTextWriter)
+  private
+    mFile: File;
+
+  protected
+    procedure putBuf (constref buf; len: SizeUInt); override;
+
+  public
+    constructor Create (const fname: AnsiString);
+    destructor Destroy (); override;
+  end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+function wcharTo1251 (wc: WideChar): AnsiChar; inline;
+function utfTo1251 (const s: AnsiString): AnsiString;
+
+function digitInBase (ch: AnsiChar; base: Integer): Integer;
+
+
+implementation
+
+uses
+  SysUtils, utils;
+
+
+var
+  wc2shitmap: array[0..65535] of AnsiChar;
+  wc2shitmapInited: Boolean = false;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+procedure initShitMap ();
+const
+  cp1251: array[0..127] of Word = (
+    $0402,$0403,$201A,$0453,$201E,$2026,$2020,$2021,$20AC,$2030,$0409,$2039,$040A,$040C,$040B,$040F,
+    $0452,$2018,$2019,$201C,$201D,$2022,$2013,$2014,$003F,$2122,$0459,$203A,$045A,$045C,$045B,$045F,
+    $00A0,$040E,$045E,$0408,$00A4,$0490,$00A6,$00A7,$0401,$00A9,$0404,$00AB,$00AC,$00AD,$00AE,$0407,
+    $00B0,$00B1,$0406,$0456,$0491,$00B5,$00B6,$00B7,$0451,$2116,$0454,$00BB,$0458,$0405,$0455,$0457,
+    $0410,$0411,$0412,$0413,$0414,$0415,$0416,$0417,$0418,$0419,$041A,$041B,$041C,$041D,$041E,$041F,
+    $0420,$0421,$0422,$0423,$0424,$0425,$0426,$0427,$0428,$0429,$042A,$042B,$042C,$042D,$042E,$042F,
+    $0430,$0431,$0432,$0433,$0434,$0435,$0436,$0437,$0438,$0439,$043A,$043B,$043C,$043D,$043E,$043F,
+    $0440,$0441,$0442,$0443,$0444,$0445,$0446,$0447,$0448,$0449,$044A,$044B,$044C,$044D,$044E,$044F
+  );
+var
+  f: Integer;
+begin
+  for f := 0 to High(wc2shitmap) do wc2shitmap[f] := '?';
+  for f := 0 to 127 do wc2shitmap[f] := AnsiChar(f);
+  for f := 0 to 127 do wc2shitmap[cp1251[f]] := AnsiChar(f+128);
+  wc2shitmapInited := true;
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+// TODO: make a hash or something
+function wcharTo1251 (wc: WideChar): AnsiChar; inline;
+begin
+  if not wc2shitmapInited then initShitMap();
+  if (LongWord(wc) > 65535) then result := '?' else result := wc2shitmap[LongWord(wc)];
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+// fast state-machine based UTF-8 decoder; using 8 bytes of memory
+// code points from invalid range will never be valid, this is the property of the state machine
+const
+  // see http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
+  utf8dfa: array[0..$16c-1] of Byte = (
+    // maps bytes to character classes
+    $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00, // 00-0f
+    $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00, // 10-1f
+    $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00, // 20-2f
+    $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00, // 30-3f
+    $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00, // 40-4f
+    $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00, // 50-5f
+    $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00, // 60-6f
+    $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00, // 70-7f
+    $01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01, // 80-8f
+    $09,$09,$09,$09,$09,$09,$09,$09,$09,$09,$09,$09,$09,$09,$09,$09, // 90-9f
+    $07,$07,$07,$07,$07,$07,$07,$07,$07,$07,$07,$07,$07,$07,$07,$07, // a0-af
+    $07,$07,$07,$07,$07,$07,$07,$07,$07,$07,$07,$07,$07,$07,$07,$07, // b0-bf
+    $08,$08,$02,$02,$02,$02,$02,$02,$02,$02,$02,$02,$02,$02,$02,$02, // c0-cf
+    $02,$02,$02,$02,$02,$02,$02,$02,$02,$02,$02,$02,$02,$02,$02,$02, // d0-df
+    $0a,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$04,$03,$03, // e0-ef
+    $0b,$06,$06,$06,$05,$08,$08,$08,$08,$08,$08,$08,$08,$08,$08,$08, // f0-ff
+    // maps a combination of a state of the automaton and a character class to a state
+    $00,$0c,$18,$24,$3c,$60,$54,$0c,$0c,$0c,$30,$48,$0c,$0c,$0c,$0c, // 100-10f
+    $0c,$0c,$0c,$0c,$0c,$0c,$0c,$0c,$0c,$00,$0c,$0c,$0c,$0c,$0c,$00, // 110-11f
+    $0c,$00,$0c,$0c,$0c,$18,$0c,$0c,$0c,$0c,$0c,$18,$0c,$18,$0c,$0c, // 120-12f
+    $0c,$0c,$0c,$0c,$0c,$0c,$0c,$18,$0c,$0c,$0c,$0c,$0c,$18,$0c,$0c, // 130-13f
+    $0c,$0c,$0c,$0c,$0c,$18,$0c,$0c,$0c,$0c,$0c,$0c,$0c,$0c,$0c,$24, // 140-14f
+    $0c,$24,$0c,$0c,$0c,$24,$0c,$0c,$0c,$0c,$0c,$24,$0c,$24,$0c,$0c, // 150-15f
+    $0c,$24,$0c,$0c,$0c,$0c,$0c,$0c,$0c,$0c,$0c,$0c);
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor TUtf8DecoderFast.Create (v: Boolean{fuck you, fpc}); begin state := Accept; codepoint := 0; end;
+
+procedure TUtf8DecoderFast.reset (); inline; begin state := Accept; codepoint := 0; end;
+
+function TUtf8DecoderFast.complete (): Boolean; inline; begin result := (state = Accept); end;
+function TUtf8DecoderFast.invalid (): Boolean; inline; begin result := (state = Reject); end;
+function TUtf8DecoderFast.completeOrInvalid (): Boolean; inline; begin result := (state = Accept) or (state = Reject); end;
+
+function TUtf8DecoderFast.decode (c: AnsiChar): Boolean; inline; overload; begin result := decode(Byte(c)); end;
+
+function TUtf8DecoderFast.decode (b: Byte): Boolean; inline; overload;
+var
+  tp: LongWord;
+begin
+  if (state = Reject) then begin state := Accept; codepoint := 0; end;
+  tp := utf8dfa[b];
+  if (state <> Accept) then codepoint := (b and $3f) or (codepoint shl 6) else codepoint := ($ff shr tp) and b;
+  state := utf8dfa[256+state+tp];
+  if (state = Reject) then begin codepoint := Replacement; state := Accept; end;
+  result := (state = Accept);
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+function utfTo1251 (const s: AnsiString): AnsiString;
+var
+  f, c: Integer;
+  ud: TUtf8DecoderFast;
+begin
+  for f := 1 to Length(s) do
+  begin
+    if (Byte(s[f]) > 127) then
+    begin
+      ud := TUtf8DecoderFast.Create(true);
+      result := '';
+      for c := 1 to Length(s) do
+      begin
+        if ud.decode(s[c]) then result += wcharTo1251(WideChar(ud.codepoint));
+      end;
+      exit;
+    end;
+  end;
+  result := s;
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+function digitInBase (ch: AnsiChar; base: Integer): Integer;
+begin
+  result := -1;
+  if (base < 1) or (base > 36) then exit;
+  if (ch < '0') then exit;
+  if (base <= 10) then
+  begin
+    if (Integer(ch) >= 48+base) then exit;
+    result := Integer(ch)-48;
+  end
+  else
+  begin
+    if (ch >= '0') and (ch <= '9') then begin result := Integer(ch)-48; exit; end;
+    if (ch >= 'a') and (ch <= 'z') then Dec(ch, 32); // poor man's tolower()
+    if (ch < 'A') or (Integer(ch) >= 65+(base-10)) then exit;
+    result := Integer(ch)-65+10;
+  end;
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+class function TTextParser.quote (const s: AnsiString): AnsiString;
+
+  function squote (const s: AnsiString): AnsiString;
+  var
+    f: Integer;
+  begin
+    result := '''';
+    for f := 1 to Length(s) do
+    begin
+      if (s[f] = '''') then result += '''';
+      result += s[f];
+    end;
+    result += '''';
+  end;
+
+  function dquote (const s: AnsiString): AnsiString;
+  var
+    f: Integer;
+    ch: AnsiChar;
+  begin
+    result := '"';
+    for f := 1 to Length(s) do
+    begin
+      ch := s[f];
+           if (ch = #0) then result += '\z'
+      else if (ch = #9) then result += '\t'
+      else if (ch = #10) then result += '\n'
+      else if (ch = #13) then result += '\r'
+      else if (ch = #27) then result += '\e'
+      else if (ch < ' ') or (ch = #127) then
+      begin
+        result += '\x';
+        result += LowerCase(IntToHex(Integer(ch), 2));
+      end
+      else if (ch = '"') or (ch = '\') then
+      begin
+        result += '\';
+        result += ch;
+      end
+      else
+      begin
+        result += ch;
+      end;
+    end;
+    result += '"';
+  end;
+
+var
+  needSingle: Boolean = false;
+  f: Integer;
+begin
+  for f := 1 to Length(s) do
+  begin
+    if (s[f] = '''') then begin needSingle := true; continue; end;
+    if (s[f] < ' ') or (s[f] = #127) then begin result := dquote(s); exit; end;
+  end;
+  if needSingle then result := squote(s) else result := ''''+s+'''';
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor TTextParser.Create (loadToken: Boolean=true);
+begin
+  mLine := 1;
+  mCol := 1;
+  mCurChar := #0;
+  mNextChar := #0;
+  mTokType := TTNone;
+  mTokStr := '';
+  mTokChar := #0;
+  mTokInt := 0;
+  mAllowSignedNumbers := true;
+  warmup(); // change `mAllowSignedNumbers` there, if necessary
+  if loadToken then skipToken();
+end;
+
+
+destructor TTextParser.Destroy ();
+begin
+  inherited;
+end;
+
+
+function TTextParser.isEOF (): Boolean; inline; begin result := (mCurChar = #0); end;
+
+
+function TTextParser.skipChar (): Boolean;
+begin
+  if (mCurChar = #0) then begin result := false; exit; end;
+  if (mCurChar = #10) then begin mCol := 1; Inc(mLine); end else Inc(mCol);
+  mCurChar := mNextChar;
+  if (mCurChar = #0) then begin result := false; exit; end;
+  loadNextChar();
+  // skip CR in CR/LF
+  if (mCurChar = #13) then
+  begin
+    if (mNextChar = #10) then loadNextChar();
+    mCurChar := #10;
+  end;
+  result := true;
+end;
+
+
+function TTextParser.skipBlanks (): Boolean;
+var
+  level: Integer;
+begin
+  while not isEOF do
+  begin
+    if (curChar = '/') then
+    begin
+      // single-line comment
+      if (nextChar = '/') then
+      begin
+        while not isEOF and (curChar <> #10) do skipChar();
+        skipChar(); // skip EOL
+        continue;
+      end;
+      // multline comment
+      if (nextChar = '*') then
+      begin
+        // skip comment start
+        skipChar();
+        skipChar();
+        while not isEOF do
+        begin
+          if (curChar = '*') and (nextChar = '/') then
+          begin
+            // skip comment end
+            skipChar();
+            skipChar();
+            break;
+          end;
+          skipChar();
+        end;
+        continue;
+      end;
+      // nesting multline comment
+      if (nextChar = '+') then
+      begin
+        // skip comment start
+        skipChar();
+        skipChar();
+        level := 1;
+        while not isEOF do
+        begin
+          if (curChar = '+') and (nextChar = '/') then
+          begin
+            // skip comment end
+            skipChar();
+            skipChar();
+            Dec(level);
+            if (level = 0) then break;
+            continue;
+          end;
+          if (curChar = '/') and (nextChar = '+') then
+          begin
+            // skip comment start
+            skipChar();
+            skipChar();
+            Inc(level);
+            continue;
+          end;
+          skipChar();
+        end;
+        continue;
+      end;
+    end;
+    if (curChar > ' ') then break;
+    skipChar(); // skip blank
+  end;
+  result := not isEOF;
+end;
+
+
+function TTextParser.skipToken (): Boolean;
+
+  procedure parseInt ();
+  var
+    neg: Boolean = false;
+    base: Integer = -1;
+    n: Integer;
+  begin
+    if mAllowSignedNumbers then
+    begin
+      if (curChar = '+') or (curChar = '-') then
+      begin
+        neg := (curChar = '-');
+        skipChar();
+        if (curChar < '0') or (curChar > '9') then
+        begin
+          mTokType := TTDelim;
+          if (neg) then mTokChar := '-' else mTokChar := '+';
+          exit;
+        end;
+      end;
+    end;
+    if (curChar = '0') then
+    begin
+      case nextChar of
+        'b','B': base := 2;
+        'o','O': base := 8;
+        'd','D': base := 10;
+        'h','H': base := 16;
+      end;
+      if (base > 0) then
+      begin
+        // skip prefix
+        skipChar();
+        skipChar();
+      end;
+    end;
+    // default base
+    if (base < 0) then base := 10;
+    if (digitInBase(curChar, base) < 0) then raise Exception.Create('invalid number');
+    mTokType := TTInt;
+    mTokInt := 0; // just in case
+    while not isEOF do
+    begin
+      n := digitInBase(curChar, base);
+      if (n < 0) then break;
+      n := mTokInt*10+n;
+      if (n < 0) or (n < mTokInt) then raise Exception.Create('integer overflow');
+      mTokInt := n;
+      skipChar();
+    end;
+    // check for valid number end
+    if not isEOF then
+    begin
+      if (curChar = '.') then raise Exception.Create('floating numbers aren''t supported yet');
+      if (curChar = '_') or ((curChar >= 'A') and (curChar <= 'Z')) or ((curChar >= 'a') and (curChar <= 'z')) or (curChar >= #128) then
+      begin
+        raise Exception.Create('invalid number');
+      end;
+    end;
+    if neg then mTokInt := -mTokInt;
+  end;
+
+  procedure parseString ();
+  var
+    qch, ch: AnsiChar;
+    n: Integer;
+  begin
+    mTokType := TTStr;
+    mTokStr := ''; // just in case
+    qch := curChar;
+    skipChar(); // skip starting quote
+    while not isEOF do
+    begin
+      // escape
+      if (qch = '"') and (curChar = '\') then
+      begin
+        if (nextChar = #0) then raise Exception.Create('unterminated string escape');
+        ch := nextChar;
+        // skip backslash and escape type
+        skipChar();
+        skipChar();
+        case ch of
+          't': mTokStr += #9;
+          'n': mTokStr += #10;
+          'r': mTokStr += #13;
+          'z': mTokStr += #0;
+          'e': mTokStr += #27;
+          'x', 'X': // hex escape
+            begin
+              n := digitInBase(curChar, 16);
+              if (n < 0) then raise Exception.Create('invalid hexstr escape');
+              skipChar();
+              if (digitInBase(curChar, 16) > 0) then
+              begin
+                n := n*16+digitInBase(curChar, 16);
+                skipChar();
+              end;
+              mTokStr += AnsiChar(n);
+            end;
+          else mTokStr += ch;
+        end;
+        continue;
+      end;
+      // duplicate single quote (pascal style)
+      if (qch = '''') and (curChar = '''') and (nextChar = '''') then
+      begin
+        // skip both quotes
+        skipChar();
+        skipChar();
+        mTokStr += '''';
+        continue;
+      end;
+      if (curChar = qch) then
+      begin
+        skipChar(); // skip ending quote
+        break;
+      end;
+      mTokStr += curChar;
+      skipChar();
+    end;
+  end;
+
+  procedure parseId ();
+  begin
+    mTokType := TTId;
+    mTokStr := ''; // just in case
+    while (curChar = '_') or ((curChar >= '0') and (curChar <= '9')) or
+          ((curChar >= 'A') and (curChar <= 'Z')) or
+          ((curChar >= 'a') and (curChar <= 'z')) or
+          (curChar >= #128) do
+    begin
+      mTokStr += curChar;
+      skipChar();
+    end;
+  end;
+
+begin
+  mTokType := TTEOF;
+  mTokStr := '';
+  mTokChar := #0;
+  mTokInt := 0;
+
+  if not skipBlanks() then
+  begin
+    result := false;
+    mTokLine := mLine;
+    mTokCol := mCol;
+    exit;
+  end;
+
+  mTokLine := mLine;
+  mTokCol := mCol;
+
+  result := true;
+
+  // number?
+  if mAllowSignedNumbers and ((curChar = '+') or (curChar = '-')) then begin parseInt(); exit; end;
+  if (curChar >= '0') and (curChar <= '9') then begin parseInt(); exit; end;
+
+  // string?
+  if (curChar = '"') or (curChar = '''') then begin parseString(); exit; end;
+
+  // identifier?
+  if (curChar = '_') or ((curChar >= 'A') and (curChar <= 'Z')) or ((curChar >= 'a') and (curChar <= 'z')) or (curChar >= #128) then begin parseId(); exit; end;
+
+  // known delimiters?
+  case curChar of
+    ',': mTokType := TTComma;
+    ':': mTokType := TTColon;
+    ';': mTokType := TTSemi;
+    '{': mTokType := TTBegin;
+    '}': mTokType := TTEnd;
+    else mTokType := TTDelim;
+  end;
+  mTokChar := curChar;
+  skipChar();
+end;
+
+
+function TTextParser.expectId (): AnsiString;
+begin
+  if (mTokType <> TTId) then raise Exception.Create('identifier expected');
+  result := mTokStr;
+  skipToken();
+end;
+
+
+procedure TTextParser.expectId (const aid: AnsiString);
+begin
+  if (mTokType <> TTId) or (CompareText(mTokStr, aid) <> 0) then raise Exception.Create('identifier '''+aid+''' expected');
+  skipToken();
+end;
+
+
+function TTextParser.eatId (const aid: AnsiString): Boolean;
+begin
+  result := false;
+  if (mTokType <> TTId) or (CompareText(mTokStr, aid) <> 0) then exit;
+  result := true;
+  skipToken();
+end;
+
+
+function TTextParser.expectStr (allowEmpty: Boolean=false): AnsiString;
+begin
+  if (mTokType <> TTStr) then raise Exception.Create('string expected');
+  if (not allowEmpty) and (Length(mTokStr) = 0) then raise Exception.Create('non-empty string expected');
+  result := mTokStr;
+  skipToken();
+end;
+
+
+function TTextParser.expectInt (): Integer;
+begin
+  if (mTokType <> TTInt) then raise Exception.Create('string expected');
+  result := mTokInt;
+  skipToken();
+end;
+
+
+procedure TTextParser.expectTT (ttype: Integer);
+begin
+  if (mTokType <> ttype) then raise Exception.Create('unexpected token');
+  skipToken();
+end;
+
+
+function TTextParser.eatTT (ttype: Integer): Boolean;
+begin
+  result := (mTokType = ttype);
+  if result then skipToken();
+end;
+
+
+function TTextParser.expectDelim (const ch: AnsiChar): AnsiChar;
+begin
+  if (mTokType <> TTDelim) then raise Exception.Create(Format('delimiter ''%s'' expected', [ch]));
+  result := mTokChar;
+  skipToken();
+end;
+
+
+function TTextParser.eatDelim (const ch: AnsiChar): Boolean;
+begin
+  result := false;
+  if (mTokType <> TTDelim) or (mTokChar <> ch) then exit;
+  result := true;
+  skipToken();
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor TFileTextParser.Create (const fname: AnsiString; loadToken: Boolean=true);
+begin
+  AssignFile(mFile, fname);
+  Reset(mFile, 1);
+  inherited Create(loadToken);
+end;
+
+
+destructor TFileTextParser.Destroy ();
+begin
+  CloseFile(mFile);
+  inherited;
+end;
+
+
+procedure TFileTextParser.warmup ();
+var
+  rd: Integer;
+begin
+  blockRead(mFile, mCurChar, 1, rd);
+  if (rd = 0) then begin mCurChar := #0; exit; end;
+  if (mCurChar = #0) then mCurChar := ' ';
+  loadNextChar();
+end;
+
+
+procedure TFileTextParser.loadNextChar ();
+var
+  rd: Integer;
+begin
+  blockRead(mFile, mNextChar, 1, rd);
+  if (rd = 0) then begin mNextChar := #0; exit; end;
+  if (mNextChar = #0) then mNextChar := ' ';
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor TStrTextParser.Create (const astr: AnsiString; loadToken: Boolean=true);
+begin
+  mStr := astr;
+  mPos := 1;
+  inherited Create(loadToken);
+end;
+
+
+destructor TStrTextParser.Destroy ();
+begin
+  mStr := '';
+  inherited;
+end;
+
+
+procedure TStrTextParser.warmup ();
+begin
+  if (mPos > Length(mStr)) then
+  begin
+    mCurChar := #0;
+    mNextChar := #0;
+    exit;
+  end;
+  mCurChar := mStr[mPos]; Inc(mPos);
+  if (mCurChar = #0) then mCurChar := ' ';
+  loadNextChar();
+end;
+
+
+procedure TStrTextParser.loadNextChar ();
+begin
+  mNextChar := #0;
+  if (mPos > Length(mStr)) then exit;
+  mNextChar := mStr[mPos]; Inc(mPos);
+  if (mNextChar = #0) then mNextChar := ' ';
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor TTextWriter.Create (); begin mIndent := 0; end;
+procedure TTextWriter.put (const s: AnsiString); overload; begin if (Length(s) > 0) then putBuf((@(s[1]))^, Length(s)); end;
+procedure TTextWriter.put (v: Byte); overload; begin put('%d', [v]); end;
+procedure TTextWriter.put (v: Integer); overload; begin put('%d', [v]); end;
+procedure TTextWriter.put (const fmt: AnsiString; args: array of const); overload; begin put(formatstrf(fmt, args)); end;
+procedure TTextWriter.putIndent (); var f: Integer; begin for f := 1 to mIndent do put(' '); end;
+procedure TTextWriter.indent (); begin Inc(mIndent, 2); end;
+procedure TTextWriter.unindent (); begin Dec(mIndent, 2); end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor TFileTextWriter.Create (const fname: AnsiString);
+begin
+  AssignFile(mFile, fname);
+  Rewrite(mFile, 1);
+  inherited Create();
+end;
+
+
+destructor TFileTextWriter.Destroy ();
+begin
+  CloseFile(mFile);
+end;
+
+
+procedure TFileTextWriter.putBuf (constref buf; len: SizeUInt);
+var
+  wr: SizeUInt;
+  pc: PChar;
+begin
+  if (len > 0) then
+  begin
+    pc := @buf;
+    BlockWrite(mFile, pc^, len, wr);
+    if (wr <> len) then raise Exception.Create('write error');
+    {
+    while (len > 0) do
+    begin
+      write(pc^);
+      Inc(pc);
+      Dec(len);
+    end;
+    }
+  end;
+end;
+
+
+end.