
{ -----------------------------------   CUT HERE --------------------   }

{$X+}
Unit CDUnit_P;

Interface

{Include the appropriate units.}

{$IfDef Windows}
{$C PRELOAD}
Uses Strings, WinCRT, WinDOS, WinProcs, SimRMI{, CD_Vars};
{$EndIf}
{$IfDef DPMI}
Uses Strings, CRT, DOS, WinAPI, SimRMI{, CD_Vars};
{$EndIf}
{$IfDef MSDOS}
Uses Strings, CRT, DOS{,CD_Vars};
{$EndIf}

type   VTOCArray  = Array[1..2048] of Byte;
       DriveByteArray = Array[1..128] of Byte;
Var
  Drive   : Integer;  { Must set drive before all operations }
  SubUnit : Integer;

type
  PAudioTrackInfo   = ^AudioTrackInfoRec;
  AudioTrackInfoRec = Record
    Track           : Integer;
    StartPoint      : LongInt;
    EndPoint        : LongInt;
    Frames,
    Seconds,
    Minutes,
    PlayMin,
    PlaySec,
    TrackControl    : Byte;
  end;

  MSCDEX_Ver_Rec = Record
    Major,
    Minor       : Integer;
  End;

  AudioDiskInfoRec = Record
    LowestTrack    : Byte;
    HighestTrack   : Byte;
    LeadOutTrack   : LongInt;
  End;

var
  NumberOfCD     : Integer;
  MSCDEX_Version : MSCDEX_Ver_Rec;
  FirstCD        : Integer;
  AudioDiskInfo  : AudioDiskInfoRec;
  RedBook,
  Audio,
  DoorOpen,
  DoorLocked,
  AudioManip,
  DiscInDrive    : Boolean;
  Busy,
  Playing,
  Paused         : Boolean;

{-------------------------------------------------------}

function File_Name(var Code : Integer) : String;

function Read_VTOC(var VTOC :VTOCArray;
                   var Index : Integer) : Boolean;

procedure CD_Check(var Code : Integer);

procedure Vol_Desc(Var Code : Integer;
                   var ErrCode : Integer);

procedure Get_Dir_Entry(PathName : String;
                        var Format, ErrCode : Integer);

procedure DeviceStatus;

procedure Audio_Channel_Info;

procedure Audio_Disk_Info;

procedure Audio_Track_Info(Var StartPoint : LongInt;
                           Var TrackControl : Byte);

procedure Audio_Status_Info;

procedure Q_Channel_Info;

procedure Lock(LockDrive : Boolean);

procedure cdReset;

procedure Eject;

procedure CloseTray;

procedure Resume_Play;

procedure Pause_Audio;

procedure Play_Audio(StartSec, EndSec : LongInt);

function StopAudio : Boolean;

function Sector_Size(ReadMode : Byte) : Word;

function Volume_Size : LongInt;

function Media_Changed : Boolean;

function Head_Location(AddrMode : Byte) : LongInt;

procedure Read_Drive_Bytes(Var ReadBytes : DriveByteArray);

function UPC_Code : String;

Implementation

Type
  ListBuf    = Record
    UnitCode : Byte;
    UnitSeg,
    UnitOfs  : Word;
  end;

  Req_Hdr    = Record
     Len     : Byte;
     SubUnit : Byte;
     Command : Byte;
     Status  : Word;
     Reserved: Array[1..8] of Byte;
  End;

Const
  Init       = 0;
  IoCtlInput = 3;
  InputFlush = 7;
  IOCtlOutput= 12;
  DevOpen    = 13;
  DevClose   = 14;
  ReadLong   = 128;
  ReadLongP  = 130;
  SeekCmd    = 131;
  PlayCD     = 132;
  StopPlay   = 133;
  ResumePlay = 136;

Type

  Audio_Play = Record
    APReq    : Req_Hdr;
    AddrMode : Byte;
    Start    : LongInt;
    NumSecs  : LongInt;
  end;

  IOControlBlock = Record
    IOReq_Hdr : Req_Hdr;
    MediaDesc : Byte;
    TransAddr : Pointer;
    NumBytes  : Word;
    StartSec  : Word;
    ReqVol    : Pointer;
    TransBlock: Array[1..130] OF Byte;
  End;

  ReadControl = Record
    IOReq_Hdr : Req_Hdr;
    AddrMode  : Byte;
    TransAddr : Pointer;
    NumSecs   : Word;
    StartSec  : LongInt;
    ReadMode  : Byte;
    IL_Size,
    IL_Skip   : Byte;
  End;

  DirBufRec    = Record
     XAR_Len   : Byte;
     FileStart : LongInt;
     BlockSize : Integer;
     FileLen   : LongInt;
     DT        : Byte;
     Flags     : Byte;
     InterSize : Byte;
     InterSkip : Byte;
     VSSN      : Integer;
     NameLen   : Byte;
     NameArray : Array[1..38] of Char;
     FileVer   : Integer;
     SysUseLen : Byte;
     SysUseData: Array[1..220] of Byte;
     FileName  : String[38];
  end;

  Q_Channel_Rec = Record
    Control     : Byte;
    Track       : Byte;
    Index       : Byte;
    Minutes     : Byte;
    Seconds     : Byte;
    Frame       : Byte;
    Zero        : Byte;
    AMinutes    : Byte;
    ASeconds    : Byte;
    AFrame      : Byte;
  End;

Var
  AudioChannel   : Array[1..9] of Byte;
  DriverList     : Array[1..26] of ListBuf;
  UnitList       : Array[1..26] of Byte;
  QChannelInfo   : Q_Channel_Rec;
  Last_Start,
  Last_End       : LongInt;
  DirBuf         : DirBufRec;

Const
  CarryFlag  = $0001;

Var
{$IfDef MSDOS}
  Regs       : Registers;
{$Else}
  Regs       :TRealModeRecord; { from SimRMI Unit }
{$EndIf}
  DOSOffset,
  DOSSegment,
  DOSSelector:Word;
  AllocateLong:Longint;
  IOBlock    : Pointer;


{$IfDef MSDOS}
{ standard DOS routines for segments and pointers }
function GetIOBlock(var Block : Pointer; Size : Word) : Boolean;
begin
  GetMem(Block, Size);
  DOSSegment := Seg(Block^);
  DOSOffset := Ofs(Block^);
  GetIOBlock := TRUE;
end;

function FreeIOBlock(var Block: Pointer) : Boolean;
begin
  FreeMem(Block, SizeOf(Block^));
  DOSSegment := 0;
  DOSSelector := 0;
  DOSOffset := 0;
  FreeIOBlock := TRUE;
end;

{$ELSE}

{ Get a block in DOS and set pointer values.  DOSSelector is used
  to access the block under protected mode.  DOSSegment accesses the
  block in real mode }

function GetIOBlock(var Block : Pointer; Size : Word) : Boolean;
begin
  AllocateLong:=GlobalDOSAlloc(Size); { enough extra room for string }
  If AllocateLong<>0 Then  {If allocation was successful...}
  Begin
     DOSSegment:=AllocateLong SHR 16;     {Get the real mode segment of the me}
     DOSSelector:=AllocateLong AND $FFFF; {Get the protected mode selector of }
     DOSOffset := 0;
     Block := Ptr(DOSSelector, 0);
     GetIOBlock := TRUE;
  End
  ELSE
     GetIOBlock := FALSE;
end;

{ Free the DOS block and dereference the pointer }

function FreeIOBlock(var Block: Pointer) : Boolean;
begin
  DOSSelector := GlobalDOSFree(DOSSelector);
  DOSSegment := 0;
  Block := NIL;
  FreeIOBlock := (DOSSelector = 0);
end;

{$EndIf}

procedure Clear_Regs;
begin
  FillChar(Regs, SizeOf(Regs), #0);
end;

procedure CD_Intr;
begin
  Regs.AH := $15;

{$IfDef MSDOS}
  Intr($2F, Regs);  { Call DOS normally }
{$Else}
  If NOT SimRealModeInt($2F,@Regs) Then    {Call DOS through the DPMI}
     Halt(100);
{$EndIf}
end;

procedure MSCDEX_Ver;
begin
  Clear_Regs;
  Regs.AL := $0C;
  Regs.BX := $0000;
  CD_Intr;
  MSCDEX_Version.Minor := 0;
  If Regs.BX = 0 Then
     MSCDEX_Version.Major := 1
  ELSE
     Begin
       MSCDEX_Version.Major := Regs.BH;
       MSCDEX_Version.Minor := Regs.BL;
     End;
end;

procedure Initialize;
begin
  NumberOfCD := 0;
  Clear_Regs;
  Regs.AL := $00;
  Regs.BX := $0000;
  CD_Intr;
  If Regs.BX <> 0 THEN
     Begin
       NumberOfCD := Regs.BX;
       FirstCD := Regs.CX;
       Clear_Regs;
       FillChar(DriverList, SizeOf(DriverList), #0);
       FillChar(UnitList, SizeOf(UnitList), #0);
       Regs.AL := $01;               { Get List of Driver Header Addresses }
       Regs.ES := Seg(DriverList);
       Regs.BX := Ofs(DriverList);
       CD_Intr;
       Clear_Regs;
       Regs.AL := $0D;               { Get List of CD-ROM Units }
       Regs.ES := Seg(UnitList);
       Regs.BX := Ofs(UnitList);
       CD_Intr;
       MSCDEX_Ver;
     End;
end;


function File_Name(var Code : Integer) : String;
Var
  FN : Pointer;
begin
  Clear_Regs;
  If NOT GetIOBlock(FN, 64) THEN
     Exit;
  FillChar(FN, SizeOf(FN), #0);
  Regs.AL := Code + 1;
{
       Copyright Filename     =  1
       Abstract Filename      =  2
       Bibliographic Filename =  3
}
  Regs.CX := Drive;
  Regs.ES := DOSSegment;
  Regs.BX := DOSOffset;
  CD_Intr;
  Code := Regs.AX;
  If (Regs.Flags AND CarryFlag) = 0 THEN
     File_Name := StrPas(FN)
  ELSE
     File_Name := '';
  FreeIOBlock(FN);
end;


function Read_VTOC(var VTOC : VTOCArray;
                   var Index : Integer) : Boolean;
{ On entry -
     Index = Vol Desc Number to read from 0 to ?
  On return
     Case Index of
            1    : Standard Volume Descriptor
            $FF  : Volume Descriptor Terminator
            0    : All others
}
var
  PVTOC : Pointer;

begin
  Clear_Regs;
  If NOT GetIOBlock(PVTOC, SizeOf(VTOCArray)) THEN
     Exit;
  FillChar(PVTOC^, SizeOf(PVTOC^), #0);
  Regs.AL := $05;
  Regs.CX := Drive;
  Regs.DX := Index;
  Regs.ES := DOSSegment;
  Regs.BX := DOSOffset;
  CD_Intr;
  Index := Regs.AX;
  Move(PVTOC^,VTOC, SizeOf(VTOC));
  If (Regs.Flags AND CarryFlag) = 0 THEN
     Read_VTOC := TRUE
  ELSE
     Read_VTOC := FALSE;
  FreeIOBlock(PVTOC);
end;

procedure CD_Check(var Code : Integer);
begin
  Clear_Regs;
  Regs.AL := $0B;
  Regs.BX := $0000;
  Regs.CX := Drive;
  CD_Intr;
  If Regs.BX <> $ADAD THEN
     Code := 2
  ELSE
     Begin
       If Regs.AX <> 0 THEN
          Code := 0
       ELSE
          Code := 1;
     End;
end;


procedure Vol_Desc(Var Code : Integer;
                   var ErrCode : Integer);

  function Get_Vol_Desc : Byte;
    begin
      Clear_Regs;
      Regs.CX := Drive;
      Regs.AL := $0E;
      Regs.BX := $0000;
      CD_Intr;
      Code := Regs.AX;
      If (Regs.Flags AND CarryFlag) <> 0 THEN
         ErrCode := $FF;
      Get_Vol_Desc := Regs.DH;
    end;

begin
  Clear_Regs;
  ErrCode := 0;
  If Code <> 0 THEN
     Begin
       Regs.DH := Code;
       Regs.DL := 0;
       Regs.BX := $0001;
       Regs.AL := $0E;
       Regs.CX := Drive;
       CD_Intr;
       Code := Regs.AX;
       If (Regs.Flags AND CarryFlag) <> 0 THEN
          ErrCode := $FF;
     End;
  If ErrCode = 0 THEN
     Code := Get_Vol_Desc;
end;

procedure Get_Dir_Entry(PathName : String;
                        var Format, ErrCode : Integer);
var
  PN : PChar;
  DB : Pointer;
begin
  FillChar(DirBuf, SizeOf(DirBuf), #0);
  PathName := PathName + #0;
  If NOT GetIOBlock(DB, SizeOf(DirBufRec) + 256) THEN
     Exit;
  PN := Ptr(DOSSelector, SizeOf(DirBufRec) + 1);
  Clear_Regs;
  Regs.AL := $0F;
  Regs.CL := Drive;
  Regs.CH := 1;
  Regs.ES := DOSSegment;
  Regs.BX := SizeOf(DirBufRec) + 1;
  Regs.SI := DOSSegment;
  Regs.DI := DOSOffset;
  CD_Intr;
  ErrCode := Regs.AX;
  If (Regs.Flags AND CarryFlag) = 0 THEN
  Begin
    Move(DB^, DirBuf, SizeOf(DirBuf));
    Move(DirBuf.NameArray[1], DirBuf.FileName[1], 38);
    DirBuf.FileName[0] := #12; { File names are only 8.3 }
    Format := Regs.AX
  End
  ELSE
    Format := $FF;
  FreeIOBlock(DB);
end;

function IO_Control(Command, NumberOfBytes, TransferBytes,
                     ReturnBytes, StartPoint : Byte;
                     var Bytes, TransferBlock): Byte;
var
  I : Word;
begin
  If NOT GetIOBlock(IOBlock, SizeOf(IOControlBlock)) THEN
     Exit;
  With IOControlBlock(IOBlock^) DO
  Begin
    I := Ofs(TransBlock[1]) - Ofs(IOReq_Hdr);
    NumBytes := NumberOfBytes;
    IOReq_Hdr.Len := 26;
    IOReq_Hdr.SubUnit := SubUnit;
    IOReq_Hdr.Status := 0;
    TransAddr := Ptr(DOSSegment, I); { 23 bytes into the IOBlock^ }
    IOReq_Hdr.Command := Command;
    Move(Bytes, TransBlock[1], TransferBytes);
    Clear_Regs;
    Regs.AL := $10;
    Regs.CX := Drive;
    Regs.ES := DOSSegment;
    Regs.BX := DOSOffset;
    CD_Intr;
    Busy := (IOReq_Hdr.Status AND 512) <> 0;
    If ((IOReq_Hdr.Status AND 32768) <> 0) THEN
       I := IOReq_Hdr.Status AND $FF
    ELSE
        I := 0;
    If ReturnBytes <> 0 THEN
       Move(TransBlock[StartPoint], TransferBlock, ReturnBytes);
  End;
  IO_Control := I;
  FreeIOBlock(IOBlock);
end;

procedure Audio_Channel_Info;
var
  Bytes : Byte;
begin
  Bytes := 4;
  IO_Control(IOCtlInput, 9, 1, 9, 1, Bytes, AudioChannel);
End;

procedure DeviceStatus;
var
  Bytes : Array[1..2] OF Byte;
  Status: Word;
begin
  Bytes[1] := 6;

  IO_Control(IOCtlInput, 5, 1, 2, 2, Bytes, Bytes);
  Move(Bytes, Status, 2);

  DoorOpen     := Status AND 1 <> 0;
  DoorLocked   := Status AND 2 = 0;
  Audio        := Status AND 16 <> 0;
  AudioManip   := Status AND 256 <> 0;
  DiscInDrive  := Status AND 2048 = 0;
  RedBook      := Status AND 1024 <> 0;
End;

procedure Audio_Disk_Info;
var Bytes : Byte;
begin
  Bytes := 10;
  IO_Control(IOCtlInput, 7, 1, 6, 2, Bytes, AudioDiskInfo);
  Playing := Busy;
end;

procedure Audio_Track_Info(Var StartPoint : LongInt;
                           Var TrackControl : Byte);
var
  Bytes : Array[1..5] Of BYTE;
begin
  Bytes[1] := 11;
  Bytes[2] := TrackControl;   { Track number }

  IO_Control(IOCtlInput, 7, 2, 5, 3, Bytes, Bytes);
  Move(Bytes[1], StartPoint, 4);
  TrackControl := Bytes[5];

  Playing := Busy;
end;

procedure Q_Channel_Info;
var
  Bytes : Byte;
begin
  Bytes := 12;
  IO_Control(IOCtlInput, 11, 1, 11, 2, Bytes, QChannelInfo);
end;

procedure Audio_Status_Info;
var
  Bytes : Array[1..11] Of Byte;
begin
  Bytes[1] := 15;
  IO_Control(IOCtlInput, 11, 1, 8, 2, Bytes, Bytes);
  Paused := (Word(Bytes[2]) AND 1) <> 0;
  Move(Bytes[4], Last_Start, 4);
  Move(Bytes[8], Last_End, 4);
  Playing := Busy;
end;

procedure Eject;
var
  Bytes : Byte;
begin
  Bytes := 0;
  IO_Control(IOCtlOutput, 1, 1, 0, 0, Bytes, Bytes);
end;

procedure cdReset;
var Bytes : Byte;
begin
  Bytes := 2;
  IO_Control(IOCtlOutput, 1, 1, 0, 0, Bytes, Bytes);
  Busy := TRUE;
end;

procedure Lock(LockDrive : Boolean);
var
  Bytes : Array[1..2] Of Byte;
begin
  Bytes[1] := 1;
  If LockDrive THEN
     Bytes[2] := 1
  ELSE
     Bytes[2] := 0;
  IO_Control(IOCtlOutput, 2, 2, 0, 0, Bytes, Bytes);
end;

procedure CloseTray;
var Bytes : Byte;
begin
  Bytes := 5;
  IO_Control(IOCtlOutput, 1, 1, 0, 0, Bytes, Bytes);
end;

Var
  AudioPlay : Pointer;


function Play(StartLoc, NumSec : LongInt) : Boolean;
begin

  If NOT GetIOBlock(AudioPlay, SizeOf(Audio_Play)) THEN
     Exit;
  With Audio_Play(AudioPlay^) DO
  Begin
    APReq.Command := PlayCD;
    APReq.Len := 22;
    APReq.SubUnit := SubUnit;
    Start := StartLoc;
    NumSecs := NumSec;
    AddrMode := 1;
    Regs.AL := $10;
    Regs.CX := Drive;
    Regs.ES := DOSSegment;
    Regs.BX := DOSOffset;
    CD_Intr;
    Play := ((APReq.Status AND 32768) = 0);
  End;
  FreeIOBlock(AudioPlay);
end;

procedure Play_Audio(StartSec, EndSec : LongInt);
Var
  SP,
  EP     : LongInt;
  SArray : Array[1..4] Of Byte;
  EArray : Array[1..4] Of Byte;
begin
  Move(StartSec, SArray[1], 4);
  Move(EndSec, EArray[1], 4);
  SP := SArray[3];           { Must use longint or get negative result }
  SP := (SP*75*60) + (SArray[2]*75) + SArray[1];
  EP := EArray[3];
  EP := (EP*75*60) + (EArray[2]*75) + EArray[1];
  EP := EP-SP;

  Playing := Play(StartSec, EP);
  Audio_Status_Info;
end;

procedure Pause_Audio;
begin

  If Playing THEN
     Begin
       If NOT GetIOBlock(AudioPlay, SizeOf(Audio_Play)) THEN
          Exit;
       With Audio_Play(AudioPlay^) DO
       Begin
         APReq.Command := StopPlay;
         APReq.Len := 13;
         APReq.SubUnit := SubUnit;
       End;
       Regs.AL := $10;
       Regs.CX := Drive;
       Regs.ES := DOSSegment;
       Regs.BX := DOSOffset;
       CD_Intr;
       FreeIOBlock(AudioPlay);
     end;
  Audio_Status_Info;
  Playing := FALSE;
end;

procedure Resume_Play;
begin
  If NOT GetIOBlock(AudioPlay, SizeOf(Audio_Play)) THEN
     Exit;
  With Audio_Play(AudioPlay^) DO
  Begin
    APReq.Command := ResumePlay;
    APReq.Len := 13;
    APReq.SubUnit := SubUnit;
  End;
  Regs.AL := $10;
  Regs.CX := Drive;
  Regs.ES := DOSSegment;
  Regs.BX := DOSOffset;
  CD_Intr;
  Audio_Status_Info;
  FreeIOBlock(AudioPlay); { free DOS block anbd dereference pointer }
end;

function StopAudio : Boolean;
begin

  If NOT GetIOBlock(AudioPlay, SizeOf(Audio_Play)) THEN
     Exit;
  With Audio_Play(AudioPlay^) DO
  Begin
    APReq.Command := StopPlay;
    APReq.Len := 13;
    APReq.SubUnit := SubUnit;
    Regs.AL := $10;
    Regs.CX := Drive;
    Regs.ES := DOSSegment;
    Regs.BX := DOSOffset;
    CD_Intr;
    StopAudio := ((APReq.Status AND 32768) = 0);
  End;
  FreeIOBlock(AudioPlay);
end;

function Sector_Size(ReadMode : Byte) : Word;
Var
  SecSize : Word;
  Bytes   : Array[1..2] Of Byte;
begin
  Bytes[1] := 7;
  Bytes[2] := ReadMode;
  IO_Control(IOCtlInput, 4, 2, 2, 3, Bytes, SecSize);
  Sector_Size := SecSize;
End;

function Volume_Size : LongInt;
Var
  VolSize : LongInt;
  Bytes   : Byte;
begin
  Bytes := 8;
  IO_Control(IOCtlInput, 5, 1, 4, 2, Bytes, VolSize);
  Volume_Size := VolSize;
End;

function Media_Changed : Boolean;

{  1  :  Media not changed
   0  :  Don't Know
  -1  :  Media changed
}
var
  MedChng : Byte;
  Bytes : Byte;
begin
  Bytes := 9;
  IO_Control(IOCtlInput, 2, 1, 4, 2, Bytes, MedChng);
  Inc(MedChng);
  If MedChng IN [1,0] THEN
     Media_Changed := True
  ELSE
     Media_Changed := False;
End;

function Head_Location(AddrMode : Byte) : LongInt;
Var
  HeadLoc : Longint;
  Bytes : Array[1..2] Of Byte;
begin
  Bytes[1] := 1;
  Bytes[2] := AddrMode;
  IO_Control(IOCtlInput, 6, 2, 4, 3, Bytes, HeadLoc);
  Head_Location := HeadLoc;
End;

procedure Read_Drive_Bytes(Var ReadBytes : DriveByteArray);
var
  Bytes : Byte;
Begin
  Bytes := 5;
  IO_Control(IOCtlInput, 130, 1, 128, 3, Bytes, ReadBytes);
End;

function UPC_Code : String;
Var
  I, J, K : Integer;
  TempStr : String;
  Bytes : Array[1..11] Of Byte;
Begin
  TempStr := '';
  FillChar(Bytes, SizeOf(Bytes), #0);
  Bytes[1] := 14;
  If (IO_Control(IOCtlInput, 11, 1, 11, 1, Bytes, Bytes) <> 0) THEN
     TempStr := 'No UPC Code'
  ELSE
  Begin
    For I := 3 to 9 DO
      Begin
        J := (Bytes[I] AND $F0) SHR 4;
        K := Bytes[I] AND $0F;
        TempStr := TempStr + Chr(J + 48);
        TempStr := TempStr + Chr(K + 48);
      End;
    If Length(TempStr) > 13 THEN
        TempStr := Copy(TempSTr, 1, 13);
  End;
  UPC_Code := TempStr;
End;

{************************************************************}
{$IfDef MSDOS}
{$ELSE}
{$F+}
var
  ExitRoutine : Pointer;
procedure MyExit;
begin
  ExitProc := ExitRoutine;
  If DOSSelector <> 0 THEN
  Begin
     GlobalDOSFree(DOSSelector);
     WriteLn('DOS Selector not free');
  End
  ELSE
     WriteLn('DOS Selector free');
end;
{$EndIf}

Begin
{------------------------------------------------}
  FillChar(DriverList, SizeOf(DriverList), #0);
  FillChar(UnitList, SizeOf(UnitList), #0);
  NumberOfCD  := 0;
  FirstCD  := 0;
  MSCDEX_Version.Major := 0;
  MSCDEX_Version.Minor := 0;
{------------------------------------------------}
  NumberOfCD := 0;
  FirstCD := 0;
  FillChar(MSCDEX_Version, SizeOf(MSCDEX_Version), #0);
  Initialize;
  Drive := FirstCD;
  SubUnit := 0;
{$IfDef MSDOS}
{$ELSE}
  ExitRoutine := ExitProc;
  ExitProc := @MyExit;
{$EndIf}
End.

