Subversion Repositories plumbers

Rev

Rev 12 | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. unit GameBinStruct;
  2.  
  3. (*
  4.  * Plumbers Don't Wear Ties - Structure of GAME.BIN
  5.  * Copyright 2017 - 2020 Daniel Marschall, ViaThinkSoft
  6.  *
  7.  * Licensed under the Apache License, Version 2.0 (the "License");
  8.  * you may not use this file except in compliance with the License.
  9.  * You may obtain a copy of the License at
  10.  *
  11.  *     http://www.apache.org/licenses/LICENSE-2.0
  12.  *
  13.  * Unless required by applicable law or agreed to in writing, software
  14.  * distributed under the License is distributed on an "AS IS" BASIS,
  15.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16.  * See the License for the specific language governing permissions and
  17.  * limitations under the License.
  18.  *)
  19.  
  20. {$A-}
  21.  
  22. interface
  23.  
  24. const
  25.   SCENEID_PREVDECISION = -1;
  26.   SCENEID_ENDGAME      = 32767;
  27.  
  28.   SEGMENT_BEGINNING = 0;
  29.   SEGMENT_DECISION  = 1;
  30.  
  31. type
  32.   PCoord = ^TCoord;
  33.   TCoord = packed record
  34.     x: Word;
  35.     y: Word;
  36.   end;
  37.  
  38.   PActionDef = ^TActionDef;
  39.   TActionDef = packed record
  40.     scoreDelta: Integer;
  41.     nextSceneID: SmallInt;  // will jump to the scene with the name "SCxx",
  42.                             // where xx stands for nextSceneID (2 digits at least)
  43.                             // 7FFF (32767) = end game
  44.                             // FFFF (   -1) = go back to the last decision
  45.     sceneSegment: SmallInt; // 0 = scene from beginning, 1 = decision page
  46.     cHotspotTopLeft: TCoord;
  47.     cHotspotBottomRight: TCoord;
  48.   end;
  49.  
  50.   PAnsiFileName = ^TAnsiFileName;
  51.   TAnsiFileName = array[0..13] of AnsiChar;
  52.  
  53.   PSceneDef = ^TSceneDef;
  54.   TSceneDef = packed record
  55.     numPics: Word;
  56.     pictureIndex: Word;
  57.     numActions: Word;
  58.     szSceneFolder: TAnsiFileName; // Foldername *must* be "SCxx" (case sensitive) where xx stands for a 2 digit ID
  59.     szDialogWav:   TAnsiFileName;
  60.     szDecisionBmp: TAnsiFileName;
  61.     actions: array[0..2] of TActionDef;
  62.   end;
  63.  
  64.   PPictureDef = ^TPictureDef;
  65.   TPictureDef = packed record
  66.     duration: Word; // deciseconds
  67.     szBitmapFile: TAnsiFileName;
  68.   end;
  69.  
  70.   PGameBinFile = ^TGameBinFile;
  71.   TGameBinFile = packed record
  72.     unknown1: array[0..6] of Word;
  73.     numScenes: Word;
  74.     numPics: Word;
  75.     unknown2: array[0..1] of Word;
  76.     scenes: array[0..99] of TSceneDef;        // Scenes start at 0x0016
  77.     pictures: array[0..1999] of TPictureDef;  // Pictures start at 0x2596
  78.  
  79.     class function MaxSceneCount: integer; static;
  80.     class function MaxPictureCount: integer; static;
  81.  
  82.     function AddSceneAtEnd(SceneID: integer): PSceneDef;
  83.     function FindScene(SceneID: integer): PSceneDef;
  84.     function FindSceneIndex(SceneID: integer): integer;
  85.     procedure DeleteScene(SceneIndex: integer);
  86.     procedure SwapScene(IndexA, IndexB: integer);
  87.     procedure DeletePicture(PictureIndex: integer);
  88.     procedure SwapPicture(IndexA, IndexB: integer);
  89.     function AddPictureBetween(Index: integer; assignToUpperScene: boolean=true): PPictureDef;
  90.     function RealPictureCount: integer;
  91.     function RealActionCount: integer;
  92.     function Defrag(OnlyCheck: boolean=false): boolean;
  93.     function BiggestUsedPictureIndex: integer;
  94.   end;
  95.  
  96. procedure _WriteStringToFilename(x: PAnsiFileName; s: AnsiString);
  97.  
  98. implementation
  99.  
  100. uses
  101.   Windows, SysUtils, Math;
  102.  
  103. procedure _WriteStringToFilename(x: PAnsiFileName; s: AnsiString);
  104. begin
  105.   ZeroMemory(x, Length(x^));
  106.   StrPLCopy(x^, s, Length(x^));
  107. end;
  108.  
  109. { TGameBinFile }
  110.  
  111. function TGameBinFile.BiggestUsedPictureIndex: integer;
  112. var
  113.   iScene, candidate: integer;
  114. begin
  115.   result := -1;
  116.   for iScene := 0 to Self.numScenes - 1 do
  117.   begin
  118.     candidate := Self.scenes[iScene].pictureIndex + Self.scenes[iScene].numPics - 1;
  119.     if candidate > result then result := candidate;
  120.   end;
  121. end;
  122.  
  123. resourcestring
  124.   S_INVALID_SCENE_ID = 'Invalid scene ID';
  125.  
  126. function TGameBinFile.AddSceneAtEnd(SceneID: integer): PSceneDef;
  127. resourcestring
  128.   S_SCENE_FULL = 'Not enough space for another scene';
  129. begin
  130.   if Self.numScenes >= Length(Self.scenes) then raise Exception.Create(S_SCENE_FULL);
  131.   if SceneID = $7FFF {Terminate program} then raise Exception.Create(S_INVALID_SCENE_ID);
  132.   if SceneID = $FFFF {Previous decision} then raise Exception.Create(S_INVALID_SCENE_ID);
  133.   result := @Self.scenes[Self.numScenes];
  134.   ZeroMemory(result, SizeOf(TSceneDef));
  135.   _WriteStringToFilename(@result.szSceneFolder, AnsiString(Format('SC%.2d', [sceneID])));
  136.   Inc(Self.numScenes);
  137. end;
  138.  
  139. function TGameBinFile.FindSceneIndex(SceneID: integer): integer;
  140. var
  141.   i: integer;
  142. begin
  143.   if SceneID = $7FFF {Terminate program} then raise Exception.Create(S_INVALID_SCENE_ID);
  144.   if SceneID = $FFFF {Previous decision} then raise Exception.Create(S_INVALID_SCENE_ID);
  145.   for i := 0 to Self.numScenes - 1 do
  146.   begin
  147.     if Self.scenes[i].szSceneFolder = Format('SC%.2d', [SceneID]) then
  148.     begin
  149.       result := i;
  150.       exit;
  151.     end;
  152.   end;
  153.   result := -1;
  154. end;
  155.  
  156. function TGameBinFile.FindScene(SceneID: integer): PSceneDef;
  157. var
  158.   idx: integer;
  159. begin
  160.   idx := FindSceneIndex(SceneID);
  161.   if idx >= 0 then
  162.     result := @Self.scenes[idx]
  163.   else
  164.     result := nil;
  165. end;
  166.  
  167. procedure TGameBinFile.DeleteScene(SceneIndex: integer);
  168. resourcestring
  169.   S_INVALID_SC_IDX = 'Invalid scene index';
  170. begin
  171.   if ((SceneIndex < 0) or (SceneIndex >= Length(Self.scenes))) then raise Exception.Create(S_INVALID_SC_IDX);
  172.   If SceneIndex < Length(Self.scenes)-1 then
  173.   begin
  174.     CopyMemory(@Self.scenes[SceneIndex], @Self.scenes[SceneIndex+1], (Length(Self.scenes)-SceneIndex-1)*SizeOf(TSceneDef));
  175.   end;
  176.   ZeroMemory(@Self.scenes[Length(Self.scenes)-1], SizeOf(TSceneDef));
  177.   Dec(Self.numScenes);
  178. end;
  179.  
  180. class function TGameBinFile.MaxPictureCount: integer;
  181. var
  182.   dummy: TGameBinFile;
  183. begin
  184.   dummy.numScenes := 1; // avoid compiler give a warning
  185.   result := Length(dummy.pictures);
  186. end;
  187.  
  188. class function TGameBinFile.MaxSceneCount: integer;
  189. var
  190.   dummy: TGameBinFile;
  191. begin
  192.   dummy.numScenes := 1; // avoid compiler give a warning
  193.   result := Length(dummy.scenes);
  194. end;
  195.  
  196. function TGameBinFile.RealActionCount: integer;
  197. var
  198.   iScene: integer;
  199. begin
  200.   result := 0;
  201.   for iScene := 0 to Self.numScenes - 1 do
  202.   begin
  203.     result := result + Self.scenes[iScene].numActions;
  204.   end;
  205. end;
  206.  
  207. function TGameBinFile.RealPictureCount: integer;
  208. var
  209.   iScene: integer;
  210. begin
  211.   result := 0;
  212.   for iScene := 0 to Self.numScenes - 1 do
  213.   begin
  214.     result := result + Self.scenes[iScene].numPics;
  215.   end;
  216. end;
  217.  
  218. procedure TGameBinFile.SwapScene(IndexA, IndexB: integer);
  219. var
  220.   bakScene: TSceneDef;
  221. begin
  222.   if IndexA = IndexB then exit;
  223.   if ((Min(IndexA, IndexB) < 0) or (Max(IndexA, IndexB) >= Length(Self.scenes))) then raise Exception.Create('Invalid scene index');
  224.   CopyMemory(@bakScene, @Self.scenes[IndexA], SizeOf(TSceneDef));
  225.   CopyMemory(@Self.scenes[IndexA], @Self.scenes[IndexB], SizeOf(TSceneDef));
  226.   CopyMemory(@Self.scenes[IndexB], @bakScene, SizeOf(TSceneDef));
  227. end;
  228.  
  229. function TGameBinFile.Defrag(OnlyCheck: boolean=false): boolean;
  230. var
  231.   iPicture, iScene, iScene2: integer;
  232.   isGap: boolean;
  233. begin
  234.   result := false;
  235.   for iPicture := MaxPictureCount - 1 downto 0 do
  236.   begin
  237.     isGap := true;
  238.     for iScene := 0 to Self.numScenes - 1 do
  239.     begin
  240.       if (iPicture >= Self.scenes[iScene].pictureIndex) and
  241.          (iPicture <= Self.scenes[iScene].pictureIndex + Self.scenes[iScene].numPics - 1) then
  242.       begin
  243.         isGap := false;
  244.         break;
  245.       end;
  246.     end;
  247.     if isGap then
  248.     begin
  249.       result := true;
  250.       if not OnlyCheck then
  251.       begin
  252.         {$REGION 'Close the gap'}
  253.         for iScene2 := 0 to Self.numScenes - 1 do
  254.         begin
  255.           if (iPicture < Self.scenes[iScene2].pictureIndex) then
  256.           begin
  257.             Dec(Self.scenes[iScene2].pictureIndex);
  258.           end;
  259.         end;
  260.         CopyMemory(@Self.pictures[iPicture], @Self.pictures[iPicture+1], (Length(Self.pictures)-iPicture-1)*SizeOf(TPictureDef));
  261.         ZeroMemory(@Self.pictures[Length(Self.pictures)-1], SizeOf(TPictureDef));
  262.         {$ENDREGION}
  263.       end;
  264.     end;
  265.   end;
  266. end;
  267.  
  268. procedure TGameBinFile.DeletePicture(PictureIndex: integer);
  269. var
  270.   iScene, iScene2: integer;
  271.   protection: integer; // prevents that two scenes get the same picture index when all pictures in a scene are deleted
  272. begin
  273.   if (PictureIndex < 0) or (PictureIndex >= Length(Self.pictures)) then raise Exception.Create('Invalid picture index');
  274.  
  275.   protection := 0;
  276.   for iScene := 0 to Self.numScenes-1 do
  277.   begin
  278.     if (PictureIndex >= Self.scenes[iScene].pictureIndex) and
  279.        (PictureIndex <= Self.scenes[iScene].pictureIndex + Self.scenes[iScene].numPics - 1) then
  280.     begin
  281.       Dec(Self.scenes[iScene].numPics);
  282.       if Self.scenes[iScene].numPics = 0 then
  283.       begin
  284.          for iScene2 := 0 to Self.numScenes-1 do
  285.          begin
  286.            if Self.scenes[iScene2].pictureIndex = PictureIndex+1 then
  287.            begin
  288.              protection := 1;
  289.              break;
  290.            end;
  291.          end;
  292.       end;
  293.     end
  294.     else if (PictureIndex+protection < Self.scenes[iScene].pictureIndex) then
  295.     begin
  296.       Dec(Self.scenes[iScene].pictureIndex);
  297.     end;
  298.   end;
  299.  
  300.   If (PictureIndex < Length(Self.pictures)-1) and (protection = 0) then
  301.   begin
  302.     CopyMemory(@Self.pictures[PictureIndex], @Self.pictures[PictureIndex+1], (Length(Self.pictures)-PictureIndex-1)*SizeOf(TPictureDef));
  303.     ZeroMemory(@Self.pictures[Length(Self.pictures)-1], SizeOf(TPictureDef));
  304.   end;
  305.  
  306.   Dec(Self.numPics);
  307. end;
  308.  
  309. procedure TGameBinFile.SwapPicture(IndexA, IndexB: integer);
  310. var
  311.   bakPicture: TPictureDef;
  312. begin
  313.   // QUE: should we forbid that a picture between "scene borders" are swapped?
  314.  
  315.   if IndexA = IndexB then exit;
  316.   if ((Min(IndexA, IndexB) < 0) or (Max(IndexA, IndexB) >= Length(Self.pictures))) then raise Exception.Create('Invalid picture index');
  317.  
  318.   CopyMemory(@bakPicture, @Self.pictures[IndexA], SizeOf(TPictureDef));
  319.   CopyMemory(@Self.pictures[IndexA], @Self.pictures[IndexB], SizeOf(TPictureDef));
  320.   CopyMemory(@Self.pictures[IndexB], @bakPicture, SizeOf(TPictureDef));
  321. end;
  322.  
  323. function TGameBinFile.AddPictureBetween(Index: integer; assignToUpperScene: boolean=true): PPictureDef;
  324.  
  325.   function _HasBuffer(Index: integer): boolean;
  326.   var
  327.     iScene: integer;
  328.   begin
  329.     for iScene := 0 to Self.numScenes-1 do
  330.     begin
  331.       if Self.scenes[iScene].pictureIndex = Index+1 then
  332.       begin
  333.         result := true;
  334.         exit;
  335.       end;
  336.     end;
  337.     result := false;
  338.   end;
  339.  
  340. resourcestring
  341.   S_INVALID_PIC_IDX = 'Invalid picture index';
  342.   S_PIC_FULL_DEFRAG = 'Not enough space for another picture. Please defrag to fill the gaps first.';
  343.   S_PIC_FULL = 'Not enough space for another picture. Maximum limit reached.';
  344. var
  345.   iScene: integer;
  346. begin
  347.   if ((Index < 0) or (Index >= Length(Self.pictures))) then raise Exception.Create(S_INVALID_PIC_IDX);
  348.  
  349.   if (BiggestUsedPictureIndex = MaxPictureCount-1) and Defrag(true) then
  350.     raise Exception.Create(S_PIC_FULL_DEFRAG);
  351.  
  352.   if Self.numPics >= MaxPictureCount then
  353.     raise Exception.Create(S_PIC_FULL);
  354.  
  355.   if assignToUpperScene then
  356.   begin
  357.     // Sc1   Sc2       Sc1   Sc2
  358.     // A B | C    ==>  A B | X C
  359.     //       ^
  360.     for iScene := 0 to Self.numScenes-1 do
  361.     begin
  362.       if (Index >= Self.scenes[iScene].pictureIndex) and
  363.          (index <= Self.scenes[iScene].pictureIndex + Max(0,Self.scenes[iScene].numPics - 1)) then
  364.       begin
  365.         Inc(Self.scenes[iScene].numPics);
  366.       end
  367.       else if (index < Self.scenes[iScene].pictureIndex) and not _HasBuffer(index) then
  368.       begin
  369.         Inc(Self.scenes[iScene].pictureIndex);
  370.       end;
  371.     end;
  372.   end
  373.   else
  374.   begin
  375.     // Sc1   Sc2       Sc1     Sc2
  376.     // A B | C    ==>  A B X | C
  377.     //       ^
  378.     for iScene := 0 to Self.numScenes-1 do
  379.     begin
  380.       if (Index >= 1 + Self.scenes[iScene].pictureIndex) and
  381.          (index <= 1 + Self.scenes[iScene].pictureIndex + Max(0,Self.scenes[iScene].numPics-1)) then
  382.       begin
  383.         Inc(Self.scenes[iScene].numPics);
  384.       end
  385.       else if (index <= Self.scenes[iScene].pictureIndex) and not _HasBuffer(index) then
  386.       begin
  387.         Inc(Self.scenes[iScene].pictureIndex);
  388.       end;
  389.     end;
  390.   end;
  391.  
  392.   result := @Self.pictures[Index];
  393.   if not _HasBuffer(index) then
  394.   begin
  395.     CopyMemory(@Self.pictures[Index+1], result, (Length(Self.pictures)-Index-1)*SizeOf(TPictureDef));
  396.   end;
  397.    
  398.   ZeroMemory(result, SizeOf(TPictureDef));
  399.  
  400.   Inc(Self.numPics);
  401. end;
  402.  
  403. end.
  404.