1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
|
// Copyright (C) 2018 Jakob L. Kreuze, All Rights Reserved.
//
// This file is part of rebuild.
//
// rebuild 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.
//
// rebuild 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
// rebuild. If not, see <http://www.gnu.org/licenses/>.
extern crate byteorder;
extern crate simple_error;
use std::error::Error;
use std::io::{Cursor, Read, Seek, SeekFrom};
use self::byteorder::{LE, ReadBytesExt};
use grp::GroupManager;
/// A rectangular chunk of raw ARGB8888 data. That is, each byte carries 8 bits
/// of information about the intensity of a certain color. The following is a
/// list of bitmasks and which color ("channel," in the vernacular) they
/// represent.
///
/// 0x000000ff - Blue
/// 0x0000ff00 - Green
/// 0x00ff0000 - Red
/// 0xff000000 - Alpha (transparency)
///
/// # Examples
///
/// Colors are extracted from each individual integer via bitwise operations.
///
/// ```
/// let bitmap = bitmaps.get(277); // Arbitrary choice of tile number.
/// let corner = bitmap.data[0];
///
/// let b = corner & 0xff;
/// let g = (corner >> 8) & 0xff;
/// let r = (corner >> 16) & 0xff;
/// let a = (corner >> 24) & 0xff;
/// ```
#[derive(Clone)]
pub struct Bitmap {
pub width: u16,
pub height: u16,
pub data: Vec<u32>,
}
/// Implementation of a bitmap cache, which is used for obtaining a bitmap
/// conversion of the individual tiles in a group file.
pub struct BitmapManager {
bitmaps: Vec<Bitmap>,
}
impl BitmapManager {
/// Create a new BitmapManager, loading all of the bitmap tiles in the given
/// GRP archive.
///
/// # Errors
///
/// This will fail if no 'PALETTE.DAT' entry exists in the GRP archive, or
/// if there is no 'TILES000.ART' entry.
pub fn new(grp: &GroupManager) -> Result<BitmapManager, Box<Error>> {
let mut bitmaps = Vec::new();
// What's the PALETTE.DAT format?
//
// char palette[768], palookup[numpalookups][256], transluc[256][256];
// short numpalookups;
//
// fil = open("PALETTE.DAT",...);
// read(fil,palette,768);
// read(fil,&numpalookups,2);
// read(fil,palookup,numpalookups*256);
// read(fil,transluc,65536);
// close(fil);
//
// PALETTE: This 768 byte array is exactly the palette you want. The
// format is: Red0, Green0, Blue0, Red1, Green1, Blue1, ..., Blue255
//
// The colors are based on the VGA 262,144 color palette. The values
// range from 0-63, so if you want to convert it to a windows palette
// you will have to multiply each byte by 4.
//
// NUMPALOOKUPS: The number of shading tables used. Usually this number
// is 32, but 16 or 64 have also been used. Each of the 256 colors of
// the VGA palette can take on any of "numpalookups" number of shades.
//
// PALOOKUP: The shading table. If numpalookups = 32, then this table
// is: (32 shades) * (256 colors) = 8192 bytes (8K). The shade tables
// are often made to go from normal brightness (shade #0) down to pitch
// black (shade #31) So the first 256 bytes of the table would be for
// shade #0, etc...
//
// TRANSLUC: 64K translucent lookup table. Given any 2 colors of the
// palette, this lookup table gives the best match of the 2 colors when
// mixed together.
//
// Here's a funny story: I noticed that Duke3D's PALETTE.DAT file is 8K
// longer than it should be. Any PALETTE.DAT file with 32 shades and
// translucent table should be 74,498 bytes. Duke3D's palette is 82,690
// bytes, but it only has 32 shades! The reason is that at one time,
// Duke3D had 64 shades in their "palookup" table. Then when we noticed
// that this extra memory overhead slowed down the frame rate of the
// game noticably, it was converted back to 32 shades. The problem is
// that my palette conversion program never truncated off the end of the
// file. So the last 8K of Duke3D's PALETTE.DAT is the last 8K of a
// translucent table that was based on an older version of their
// palette.
//
// For canonical parsers, see:
// - 'paletteLoadFromDisk' in EDuke's 'build/src/palette.cpp'
// - 'loadpalette' in Build's 'ENGINE.C'
// - 'LoadPalette' in Transfusion's 'arttools/art2tga.c'
let palette = if let Some(data) = grp.get("PALETTE.DAT") {
let mut palette = Vec::new();
for i in 0..256 {
// The VGA 262,144 color palette appears darker than intended
// when interpreted ARGB8888 output, so the intensity of the
// red, green, and blue channels are scaled by a uniform factor.
let a = (0xff) as u32;
let r = (data[(i * 3)] << 2) as u32;
let g = (data[(i * 3) + 1] << 2) as u32;
let b = (data[(i * 3) + 2] << 2) as u32;
palette.push(a << 24 | r << 16 | g << 8 | b);
}
palette
} else {
// FIXME: There should be a default palette to fall back on. Update
// the documentation for this method when that's implemented.
bail!("No PALETTE.DAT in GRP archive.");
};
// From BUILDINF.TXT
//
// All art files must have xxxxx###.ART. When loading an art file you
// should keep trying to open new xxxxx###'s, incrementing the number,
// until an art file is not found.
for i in 0.. {
if let Some(data) = grp.get(&format!("TILES{:03}.ART", i)) {
// FIXME: Passing the Error back up the stack is suboptimal (?)
let parsed = BitmapManager::load_art(data, &palette)?;
bitmaps.extend_from_slice(&parsed);
} else {
// Indicates that we didn't even load TILES000.ART, meaning that
// literally NO bitmaps were loaded. That's a pretty big issue.
if i == 0 {
bail!("No TILES000.ART in GRP archive.");
}
// But if i > 0, we simply hit the last tilesheet. No problem.
break;
}
}
Ok(BitmapManager { bitmaps })
}
/// Load the tile given by the specified index, or None if no tile with the
/// specified index exists.
pub fn get(&self, index: i32) -> Option<&Bitmap> {
let index = index as usize;
if index < self.bitmaps.len() {
Some(&self.bitmaps[index])
} else {
None
}
}
/// Loads the tiles in an TILES###.ART file.
fn load_art(data: &[u8], palette: &[u32]) -> Result<Vec<Bitmap>, Box<Error>> {
let len = data.len() as u32;
if len < 16 {
bail!("ART does not contain a valid header.");
}
let mut data = Cursor::new(data);
let mut bitmaps = Vec::new();
// From BUILDINF.TXT
//
// 1. long artversion;
//
// The first 4 bytes in the art format are the version number. The
// current current art version is now 1. If artversion is not 1 then
// either it's the wrong art version or something is wrong.
let version = data.read_u32::<LE>()?;
if version != 1 {
bail!("Invalid ART version.");
}
// 2. long numtiles;
//
// Numtiles is not really used anymore. I wouldn't trust it. Actually
// when I originally planning art version 1 many months ago, I thought I
// would need this variable, but it turned it is was unnecessary. To get
// the number of tiles, you should search all art files, and check the
// localtilestart and localtileend values for each file.
let _count = data.read_u32::<LE>()?;
// 3. long localtilestart;
//
// Localtilestart is the tile number of the first tile in this art file.
//
// 4. long localtileend;
//
// Localtileend is the tile number of the last tile in this art file.
// Note: Localtileend CAN be higher than the last used slot in an art
// file.
//
// Example: If you chose 256 tiles per art file:
// TILES000.ART -> localtilestart = 0, localtileend = 255
// TILES001.ART -> localtilestart = 256, localtileend = 511
// TILES002.ART -> localtilestart = 512, localtileend = 767
// TILES003.ART -> localtilestart = 768, localtileend = 1023
let first_tile_index = data.read_u32::<LE>()?;
let last_tile_index = data.read_u32::<LE>()?;
let count = last_tile_index - first_tile_index + 1;
if len < 8 * count + 16 {
bail!(format!("Invalid number of tiles (given: {})", count));
}
// 5. short tilesizx[localtileend-localtilestart+1];
//
// This is an array of shorts of all the x dimensions of the tiles in this art
// file. If you chose 256 tiles per art file then [localtileend-localtilestart+1]
// should equal 256.
//
// 6. short tilesizy[localtileend-localtilestart+1];
//
// This is an array of shorts of all the y dimensions.
//
// 7. long picanm[localtileend-localtilestart+1];
//
// This array of longs stores a few attributes for each tile that you
// can set inside EDITART. You probably won't be touching this array,
// but I'll document it anyway.
//
// Bit: |31 24|23 16|15 8|7 0|
// -----------------------------------------------------------------
// | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
// -----------------------------------------------------------------
// | Anim. | Signed char | Signed char | | Animate type:|
// | Speed | Y-center | X-center | | 00 - NoAnm |
// |-------| offset | offset | | 01 - Oscil |
// |---------------|---------------| | 10 - AnmFd |
// | 11 - AnmBk |
// |--------------|
//
// You probably recognize these:
// Animate speed - EDITART key: 'A', + and - to adjust
// Signed char x&y offset - EDITART key: '`', Arrows to adjust
// Animate number&type - EDITART key: +/- on keypad
//
// 8. After the picanm's, the rest of the file is straight-forward rectangular art
// data. You must go through the tilesizx and tilesizy arrays to find where the
// artwork is actually stored in this file.
//
// Note: The tiles are stored in the opposite coordinate system than the screen
// memory is stored. Example on a 4*4 file:
//
// Offsets:
// ---------------------
// | 0 | 4 | 8 | 12 |
// ---------------------
// | 1 | 5 | 9 | 13 |
// ---------------------
// | 2 | 6 | 10 | 14 |
// ---------------------
// | 3 | 7 | 11 | 15 |
// ---------------------
// 16 bytes for the header, 2 bytes per entry for the array of bitmap
// widths, 2 bytes per entry for the array of bitmap heights, and 4
// bytes per entry for the array of bitmap attributes.
let data_off = 16 + 2 * count + 2 * count + 4 * count;
let mut data_off = data_off as u64;
if (len as u64) < data_off {
bail!(format!("Invalid number of tiles (given: {})", count));
}
for i in 0..count {
let width_array_off = 2 * i + 16;
let width_array_off = width_array_off as u64;
data.seek(SeekFrom::Start(width_array_off))?;
let width = data.read_u16::<LE>()? as usize;
let height_array_off = 2 * i + 2 * count + 16;
let height_array_off = height_array_off as u64;
data.seek(SeekFrom::Start(height_array_off))?;
let height = data.read_u16::<LE>()? as usize;
let mut indices = vec![0; width * height];
data.seek(SeekFrom::Start(data_off))?;
data.read(&mut indices)?;
data_off += indices.len() as u64;
let mut data = Vec::new();
for column in 0..width {
for row in 0..height {
let index = indices[row * width + column];
data.push(palette[index as usize]);
}
}
let width = width as u16;
let height = height as u16;
bitmaps.push(Bitmap { width, height, data });
}
Ok(bitmaps)
}
}
// FIXME: This documentation is bare and undescriptive.
/// Loads a font blob into a Bitmap.
pub fn load_font(font: &[u8]) -> Bitmap {
// TODO: There is no error checking, as I plan to dynamically generate a
// number of glyphs and appropriate dimensions from the size of 'data'.
// FIXME: Width and height constants are arbitrary and should ideally be
// dynamically calculated for a set of glyphs with arbitrary length.
let width = 128;
let height = 256;
let mut data = vec![0; height * width];
// FIXME: Again, this MAX_GLYPH, which isn't even referred to by a static
// constant, should be dynamically calculated.
for glyph in 0..256 {
let x_off = (glyph % 32) * 8;
let y_off = (glyph / 32) * 8;
for i in 0..8 {
for j in 0..8 {
let byte = font[(glyph * 8 + i) as usize];
let bit = 2 << (7 - j);
if byte & bit != 0 {
// The font files don't convey any color information, just
// the pixel that's set, so we default to a plain white.
let pixel = 0xffffffff;
let x = x_off + i;
let y = y_off + j;
data[x * width + y] = pixel;
}
}
}
}
Bitmap { width: width as u16, height: height as u16, data }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_slice() {
// Generated palette blob. All colors in the palette are white (HTML hex
// ffffffff).
let palette = vec![0xffffffff; 256];
// Binary blob containing an ART test vector, made by me. Contains one
// tile, a single black pixel.
let data = vec![
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
];
let parsed = BitmapManager::load_art(&data, &palette).unwrap();
assert_eq!(parsed.len(), 1);
let tile = &parsed[0];
assert_eq!(tile.width, 1);
assert_eq!(tile.height, 1);
assert_eq!(tile.data[0], 0xffffffff);
}
#[test]
fn test_incomplete_header() {
let palette = vec![0xffffffff; 256];
// Binary blob similar to the ART test vector above, but without the
// actual bitmap data.
let data = vec![
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
];
if let Ok(_) = BitmapManager::load_art(&data, &palette) {
panic!("Parser accepted invalid ART file.");
}
}
}
|