From: Ketmar Dark Date: Sat, 26 Aug 2017 00:08:48 +0000 (+0300) Subject: preliminary textual map framework; DO NOT USE! X-Git-Url: http://deadsoftware.ru/gitweb?a=commitdiff_plain;h=cead2891e0ba7e60639a60af7142eb144ab88ee4;p=d2df-sdl.git preliminary textual map framework; DO NOT USE! --- diff --git a/src/shared/xdynrec.pas b/src/shared/xdynrec.pas new file mode 100644 index 0000000..71a95bf --- /dev/null +++ b/src/shared/xdynrec.pas @@ -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 . + *) +{$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 index 0000000..503a9a0 --- /dev/null +++ b/src/shared/xparser.pas @@ -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 . + *) +{$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.