Subversion Repositories plumbers

Rev

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

Rev Author Line No. Line
2 daniel-mar 1
unit GameBinStruct;
2
 
25 daniel-mar 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
 
2 daniel-mar 20
{$A-}
21
 
22
interface
23
 
24
const
25
  SCENEID_PREVDECISION = -1;
26
  SCENEID_ENDGAME      = 32767;
27
 
12 daniel-mar 28
  SEGMENT_BEGINNING = 0;
29
  SEGMENT_DECISION  = 1;
30
 
2 daniel-mar 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;
12 daniel-mar 41
    nextSceneID: SmallInt;  // will jump to the scene with the name "SCxx",
42
                            // where xx stands for nextSceneID (2 digits at least)
2 daniel-mar 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
10 daniel-mar 78
 
11 daniel-mar 79
    class function MaxSceneCount: integer; static;
80
    class function MaxPictureCount: integer; static;
81
 
2 daniel-mar 82
    function AddSceneAtEnd(SceneID: integer): PSceneDef;
12 daniel-mar 83
    function FindScene(SceneID: integer): PSceneDef;
84
    function FindSceneIndex(SceneID: integer): integer;
2 daniel-mar 85
    procedure DeleteScene(SceneIndex: integer);
86
    procedure SwapScene(IndexA, IndexB: integer);
87
    procedure DeletePicture(PictureIndex: integer);
88
    procedure SwapPicture(IndexA, IndexB: integer);
10 daniel-mar 89
    function AddPictureBetween(Index: integer; assignToUpperScene: boolean=true): PPictureDef;
90
    function RealPictureCount: integer;
91
    function RealActionCount: integer;
11 daniel-mar 92
    function Defrag(OnlyCheck: boolean=false): boolean;
93
    function BiggestUsedPictureIndex: integer;
2 daniel-mar 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
 
12 daniel-mar 109
{ TGameBinFile }
110
 
11 daniel-mar 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
 
12 daniel-mar 123
resourcestring
124
  S_INVALID_SCENE_ID = 'Invalid scene ID';
125
 
2 daniel-mar 126
function TGameBinFile.AddSceneAtEnd(SceneID: integer): PSceneDef;
12 daniel-mar 127
resourcestring
128
  S_SCENE_FULL = 'Not enough space for another scene';
2 daniel-mar 129
begin
12 daniel-mar 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);
2 daniel-mar 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
 
12 daniel-mar 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
 
2 daniel-mar 167
procedure TGameBinFile.DeleteScene(SceneIndex: integer);
12 daniel-mar 168
resourcestring
169
  S_INVALID_SC_IDX = 'Invalid scene index';
2 daniel-mar 170
begin
12 daniel-mar 171
  if ((SceneIndex < 0) or (SceneIndex >= Length(Self.scenes))) then raise Exception.Create(S_INVALID_SC_IDX);
2 daniel-mar 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));
10 daniel-mar 177
  Dec(Self.numScenes);
2 daniel-mar 178
end;
179
 
11 daniel-mar 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
 
10 daniel-mar 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
 
2 daniel-mar 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
 
11 daniel-mar 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
 
2 daniel-mar 268
procedure TGameBinFile.DeletePicture(PictureIndex: integer);
269
var
10 daniel-mar 270
  iScene, iScene2: integer;
271
  protection: integer; // prevents that two scenes get the same picture index when all pictures in a scene are deleted
2 daniel-mar 272
begin
273
  if (PictureIndex < 0) or (PictureIndex >= Length(Self.pictures)) then raise Exception.Create('Invalid picture index');
274
 
10 daniel-mar 275
  protection := 0;
2 daniel-mar 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);
10 daniel-mar 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;
2 daniel-mar 293
    end
10 daniel-mar 294
    else if (PictureIndex+protection < Self.scenes[iScene].pictureIndex) then
2 daniel-mar 295
    begin
296
      Dec(Self.scenes[iScene].pictureIndex);
297
    end;
298
  end;
299
 
11 daniel-mar 300
  If (PictureIndex < Length(Self.pictures)-1) and (protection = 0) then
2 daniel-mar 301
  begin
11 daniel-mar 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));
2 daniel-mar 304
  end;
10 daniel-mar 305
 
306
  Dec(Self.numPics);
2 daniel-mar 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
 
10 daniel-mar 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
 
12 daniel-mar 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.';
2 daniel-mar 344
var
345
  iScene: integer;
346
begin
12 daniel-mar 347
  if ((Index < 0) or (Index >= Length(Self.pictures))) then raise Exception.Create(S_INVALID_PIC_IDX);
2 daniel-mar 348
 
11 daniel-mar 349
  if (BiggestUsedPictureIndex = MaxPictureCount-1) and Defrag(true) then
12 daniel-mar 350
    raise Exception.Create(S_PIC_FULL_DEFRAG);
11 daniel-mar 351
 
352
  if Self.numPics >= MaxPictureCount then
12 daniel-mar 353
    raise Exception.Create(S_PIC_FULL);
11 daniel-mar 354
 
10 daniel-mar 355
  if assignToUpperScene then
2 daniel-mar 356
  begin
10 daniel-mar 357
    // Sc1   Sc2       Sc1   Sc2
358
    // A B | C    ==>  A B | X C
359
    //       ^
360
    for iScene := 0 to Self.numScenes-1 do
2 daniel-mar 361
    begin
10 daniel-mar 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
2 daniel-mar 379
    begin
10 daniel-mar 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;
2 daniel-mar 389
    end;
390
  end;
391
 
392
  result := @Self.pictures[Index];
10 daniel-mar 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
 
2 daniel-mar 398
  ZeroMemory(result, SizeOf(TPictureDef));
10 daniel-mar 399
 
400
  Inc(Self.numPics);
2 daniel-mar 401
end;
402
 
403
end.