program xvol;
{
   Program to extract individual files from an AGI version 3 VOL file.

   Written by Lance Ewing.

   (Why pascal? Hey, that's what I was into at the time).
}

uses Dos;

const
 fileNames : array[1..4] of string[9] = (
  'LOGIC.', 'PICTURE.', 'VIEW.', 'SOUND.'
 );
 MAXBITS = 12;
 TABLE_SIZE = 18041;
 START_BITS = 9;

type
 prefix_code_array = array[0..TABLE_SIZE] of word;
 append_character_array = array[0..TABLE_SIZE] of char;
 prefix_code_type = ^prefix_code_array;
 append_character_type = ^append_character_array;
 FileType = file of char;

var
 BITS, MAX_VALUE, MAX_CODE : integer;
 prefix_code : prefix_code_type;
 append_character : append_character_type;
 input_bit_count : integer;
 input_bit_buffer : longint;

function SetBITS(Value:integer):boolean;
begin
 if (Value = MAXBITS) then
  SetBITS := TRUE
 else
  begin
   BITS := Value;
   MAX_VALUE := (1 SHL BITS) - 1;
   MAX_CODE := MAX_VALUE - 1;
   SetBITS := FALSE;
  end;
end;

function decode_string(AString:string; code: word) :string;
var
 i : integer;
begin
 i := 0;
 While (code > 255) do
 begin
  AString := concat(AString, append_character^[code]);
  code := prefix_code^[code];
  i := i + 1;
  if (i > 4000) then
  begin
   WriteLn('Fatal error during code expansion.');
   Halt(1);
  end;
 end;
 AString := concat(AString, Chr(ord(code)));
 decode_string := AString;
end;

function input_code(var input:FileType) : word;
var
 return_value:word;
 CharacterInput : char;
 AWord:word;
begin
 While ((input_bit_count <= 24) and (not EOF(input))) do
 begin
  Read(input, CharacterInput);
  AWord := ORD(CharacterInput);
  input_bit_buffer := input_bit_buffer or (AWord SHL input_bit_count);
  input_bit_count := input_bit_count + 8;
 end;
 return_value := (input_bit_buffer and $7FFF) MOD (1 SHL BITS);
 input_bit_buffer := input_bit_buffer SHR BITS;
 input_bit_count := input_bit_count - BITS;
 if EOF(input) then return_value := MAX_VALUE;
 input_code := return_value;
end;

procedure expand(var input, output : FileType; fileLength : word);
var
 next_code, new_code, old_code : word;
 character, counter, index : integer;
 AString : string;
 AChar :char;
 BITSFull : boolean;
 i : integer;
begin
 BITSFull := SetBITS(START_BITS);   {Set initial value of BITS}

 next_code := 257;                {Next available code to define. SIERRA start at 257}
                                  {256 is apparently the table flush code}
 counter := 0;                    {counter is a pacifier}
 WriteLn('Expanding...');

 old_code := input_code(input);   {Read in the first code.}
 AChar := chr(ord(old_code));
 character := old_code;           {Initialise the character variable.}
 new_code := input_code(input);

 While (filepos(output) < fileLength) do
 begin

  if (counter = 1000) then    { Make it look like somethings being done }
  begin
   counter := 0;
   Write('*');
  end;
  counter := counter + 1;

  if (new_code = $100) then   { Restart LZW process }
  begin

   next_code := 258;
   BITSFull := SetBITS(START_BITS);
   old_code := input_code(input);   {Read in the first code.}
   AChar := chr(ord(old_code));
   character := old_code;           {Initialise the character variable.}
   Write(output, AChar);            {Send out the first code.}
   new_code := input_code(input);

  end
  else
  begin

   if (new_code >= next_code) then
    AString := decode_string(Chr(character), old_code)
   else
    AString := decode_string('', new_code);

   character := ord(AString[length(AString)]);
   for index := length(AString) downto 1 do
    Write(output, AString[index]);

   if (next_code > MAX_CODE) then
    BITSFull := SetBITS(BITS + 1);

   prefix_code^[next_code] := old_code;
   append_character^[next_code] := Chr(character);
   next_code := next_code + 1;
   old_code := new_code;
   AString := '';
   new_code := input_code(input);

  end;
 end;
end;

function findGameSig: string;
var
   DirInfo: SearchRec;
   dirString: string;
   volString: string;
begin
   dirString := '';
   volString := '';

   FindFirst('*.?', Archive, DirInfo);
   while DosError = 0 do
   begin
      if (pos('DIR', DirInfo.Name) > 1) then
         dirString := copy(DirInfo.Name, 1, pos('DIR', DirInfo.Name)-1);
      if (pos('VOL.0', DirInfo.Name) > 1) then
         volString := copy(DirInfo.Name, 1, pos('VOL.0', DirInfo.Name)-1);
      FindNext(DirInfo);
   end;

   if ((volString = dirString) and (volString <> '')) then
      findGameSig := volString
   else
      findGameSig := '';
end;

procedure extractFile(agiFileType, fileNum : integer);
var
   gameSig, dirFileName, volFileName, volFileNum: string;
   dirFile, volFile, tempFile : file of byte;
   dirOffsetLo, dirOffsetHi, firstByte, secondByte, thirdByte: byte;
   fLenHi, fLenLo, filebyte, decompLo, decompHi, compType : byte;
   fLen, dirFilePos, decompSize : word;
   volFilePos, calcLongInt: longint;
   transferChar : char;
   sierraFile, dumpFile : FileType;
   dumpFileNum, dumpFileName : string;
begin
   gameSig := findGameSig;
   if (gameSig = '') then
   begin
      writeln('Error locating version 3 game files!');
      writeln('Make sure you run XV3 in a directory');
      writeln('containing a game which uses AGI v3.');
      halt;
   end;

   dirFileName := concat(gameSig, 'DIR');
   {$I-}
   assign(dirFile, dirFileName);
   reset(dirFile);
   {$I+}
   if (IOResult = 0) then
   begin
      seek(dirFile, (agiFileType * 2));
      read(dirFile, dirOffsetLo, dirOffsetHi);
      seek(dirFile, ((dirOffsetHi * 256) + dirOffsetLo) + (fileNum*3));
      read(dirFile, firstByte, secondByte, thirdByte);
      close(dirFile);

      if ((firstByte = $FF) and (secondByte = $FF) and (thirdByte = $FF)) then
      begin
         writeln('File doesn''t exist');
         halt;
      end
      else
      begin
         calcLongInt := firstByte;
         volFilePos := (calcLongInt and $F) shl 16;
         calcLongInt := secondByte;
         volFilePos := volFilePos + (calcLongInt shl 8);
         calcLongInt := thirdByte;
         volFilePos := volFilePos + calcLongInt;
         str((firstByte and $F0) shr 4, volFileNum);
         volFileName := concat(gameSig, 'VOL.', volFileNum);

         {$I-}
         assign(volFile, volFileName);
         reset(volFile);
         {$I+}
         if (IOResult <> 0) then
         begin
            writeln('Error opening vol file : ',volFileName);
            halt;
         end;

         seek(volFile, volFilePos + 2);
         read(volFile, compType, decompLo, decompHi, fLenLo, fLenHi);
         decompSize := (decompHi * 256) + decompLo;
         fLen := (fLenHi * 256) + fLenLo;

         {$I-}
         assign(tempFile, 'TEMP');
         rewrite(tempFile);
         {$I+}
         if (IOResult <> 0) then
         begin
            writeln('Error opening TEMP file.');
            halt;
         end;

         for dirFilePos := 0 to fLen - 1 do
         begin
            read(volFile, fileByte);
            write(tempFile, fileByte);
         end;
         close(tempFile);
         close(volFile);


         { DECOMPRESS TEMP FILE }

         input_bit_buffer := 0;
         input_bit_count := 0;

         NEW(prefix_code);
         NEW(append_character);
         if (prefix_code = nil) or (append_character = nil) then
         begin
            WriteLn('Fatal error allocating table space!');
            Halt(1);
         end;

         assign(sierraFile, 'TEMP');
         reset(sierraFile);

         str(fileNum, dumpFileNum);
         dumpFileName := concat(fileNames[agiFileType + 1], dumpFileNum);
         {$I-}
         assign(dumpFile, dumpFileName);
         rewrite(dumpFile);
         {$I+}
         if (IOResult <> 0) then
         begin
            writeln('Error opening dump file : ',dumpFileName);
            halt;
         end;

         if ((compType and $80) <> $80) then
            expand(sierraFile, dumpFile, decompSize)
         else
            while (not eof(sierraFile)) do
            begin
               read(sierraFile, transferChar);
               write(dumpFile, transferChar);
            end;

         close(sierraFile);
         close(dumpFile);
         DISPOSE(prefix_code);
         DISPOSE(append_character);
      end;
   end
   else
   begin
      writeln('Error opening DIR file : ',dirFileName);
      halt;
   end;
end;

var
   fileNum, code : integer;
   option : string;
begin

   if (paramcount < 2) then
   begin
      writeln('Usage: xv3 -l logicnumber');
      writeln('       xv3 -s soundnumber');
      writeln('       xv3 -v viewnumber');
      writeln('       xv3 -p picturenumber');
   end
   else
   begin

      option := paramstr(1);

      if (option[1] = '-') then
      begin

	       val(paramstr(2), fileNum, code);
         if (code <> 0) then
         begin
            writeln('Error at position : ',code);
            halt;
         end;

	       if (fileNum >= 0) then
	       begin

	          case (option[2]) of

	             'l': extractFile(0, fileNum);
	             's': extractFile(3, fileNum);
	             'v': extractFile(2, fileNum);
	             'p': extractFile(1, fileNum);

	          else
               writeln('Invalid option : ', paramstr(1));
	          end;

	       end
	       else
	          writeln('File number must be a postive number.');
      end
      else
	       writeln('Invalid option : ', paramstr(1));

   end;

end.
