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
//! Crate for parsing ElastoMania LGR files.
//!
//! LGR files contain PCX images.
//!
//! Example usage:
//!
//!     use elma_lgr::Lgr;
//!
//!     let lgr = Lgr::load_from_file("lgr/example.lgr", false, false).unwrap();
//!     println!("There are {} images in this LGR file", lgr.images.len());
//!     for (name, image) in lgr.images {
//!         println!("{}, width = {}, height = {}", name, image.width, image.height);
//!     }
//!
extern crate byteorder;
extern crate pcx;

use std::{io, iter};
use std::path::Path;
use std::fs::File;
use std::collections::BTreeMap;
use byteorder::{LittleEndian, ReadBytesExt};

/// Kind of a picture.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum PictureKind {
    /// A simple picture with the top-left pixel signing the transparent color.
    Picture,

    /// Texture.
    Texture,

    /// Masks are used to draw textures.
    Mask,
}

/// Clipping property.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ClippingMode {
    /// `Unclipped` means that the picture or texture will be seen above the whole picture (unless other pictures hide it).
    Unclipped,

    /// `Ground` means the picture will be seen only above the ground.
    Ground,

    /// `Sky` means the picture will be seen only above the sky.
    Sky,
}

/// Picture description.
#[derive(Debug, Clone, Copy)]
pub struct Picture {
    /// Whether this is picture, texture or mask.
    pub kind : PictureKind,

    /// The distance must be in the 1-999 range. If a picture has less distance than an other, it will hide the other picture.
    /// The motorbiker and the food-exit-killer objects have a distance of 500.
    pub distance : u32,

    /// The clipping property determines whether a picture is seen above the sky, the ground, or both (this is independent of the distances).
    pub clipping : ClippingMode,
}

/// Image from LGR.
#[derive(Debug, Clone)]
pub struct Image {
    /// Optional information describing image.
    pub info : Option<Picture>,

    /// Width of the image.
    pub width : u16,

    /// Height of the image.
    pub height : u16,

    /// Image pixels. Each pixel is an index into LGR palette.
    ///
    /// This array will only contain values if LGR was loaded with `load_pixels` option.
    pub pixels : Vec<u8>,

    /// Raw content of the PCX file.
    ///
    /// This array will only contain values if LGR was loaded with `load_raw_pcx` option.
    pub pcx : Vec<u8>,
}

/// Content of an LGR file.
#[derive(Debug, Clone)]
pub struct Lgr {
    /// All images contained in this LGR. Names include .pcx extension.
    pub images : BTreeMap <String, Image>,

    /// Palette contains 256-colors, format is R0, G0, B0, R1, G1, B1, ...
    pub palette : Vec<u8>,
}

impl Image {
    /// Get image pixel. Returned value is an index into LGR palette.
    ///
    /// This function will panic if `x >= width`, `y >= height` or LGR was loaded with `load_pixels` set to `false`.
    #[inline]
    pub fn get_pixel(&self, x : u16, y : u16) -> u8 {
        self.pixels[(y as usize)*(self.width as usize) + (x as usize)]
    }
}

impl Lgr {
    /// Get color from LGR palette. There are 256 colors in the palette.
    ///
    /// Returns triple (R, G, B).
    #[inline]
    pub fn get_palette_color(&self, i : u8) -> (u8, u8, u8) {
        (
            self.palette[(i as usize)*3],
            self.palette[(i as usize)*3 + 1],
            self.palette[(i as usize)*3 + 2]
        )
    }

    /// Read LGR from file.
    ///
    /// Arguments:
    ///
    /// * `load_pixels` - load pixels from PCX images and store them into `Image::pixels`.
    /// * `load_raw_pcx` - load raw byte content of PCX images into `Image::pcx`.
    ///
    /// If you are going to use LGR for rendering then set `load_pixels` to `true` and `load_raw_pcx` to `false`.
    /// If you wat to extract files from LGR then set `load_pixels` to `false` and `load_raw_pcx` to `true`.
    pub fn load_from_file<P: AsRef<Path>>(path: P, load_pixels: bool, load_raw_pcx: bool) -> io::Result<Self> {
        let file = File::open(path)?;
        Self::load_from_stream(&mut io::BufReader::new(file), load_pixels, load_raw_pcx)
    }

    /// Read LGR from stream.
    ///
    /// See description of `load_from_file` for more info.
    pub fn load_from_stream<R : io::Read>(stream : &mut R, load_pixels: bool, load_raw_pcx: bool) -> io::Result<Self> {
        let mut magic  = [0; 5];
        stream.read_exact(&mut magic)?;

        if &magic != b"LGR12" {
            return error("Not an LGR");
        }

        let total_images = stream.read_u32::<LittleEndian>()? as usize;
        let unknown = stream.read_u32::<LittleEndian>()?;
        if unknown != 1002 { // some kind of version or something like that
            return error("LGR: invalid unknown value != 1002");
        }

        let listed_images = stream.read_u32::<LittleEndian>()? as usize;

        let mut infos = read_pictures(stream, listed_images)?;

        let mut images = BTreeMap::new();
        let mut palette = Vec::new();
        for _ in 0..total_images {
            let name = read_string(stream, 12)?;
            let _unknown_a = stream.read_i32::<LittleEndian>()?;
            let _unknown_b = stream.read_i32::<LittleEndian>()?;
            let length = stream.read_u32::<LittleEndian>()? as usize;

            let mut pcx : Vec<u8> = std::iter::repeat(0).take(length).collect();
            stream.read_exact(&mut pcx)?;

            let info = infos.remove(name.trim_right_matches(".pcx"));

            let (pixels, width, height) = {
                let mut pcx_reader = pcx::Reader::new(&pcx[..])?;
                let (width, height) = (pcx_reader.width() as usize, pcx_reader.height() as usize);

                let pixels = if load_pixels {
                    let mut pixels: Vec<u8> = iter::repeat(0).take(width*height).collect();

                    for i in 0..height {
                        pcx_reader.next_row_paletted(&mut pixels[i*width..(i + 1)*width])?;
                    }

                    pixels
                } else {
                    Vec::new()
                };

                // Masks contain invalid palettes, we can take palette from the first image that is not a mask.
                let valid_palette = if let Some(info) = info {
                    info.kind != PictureKind::Mask
                } else {
                    true
                };

                if valid_palette && palette.is_empty() {
                    palette = iter::repeat(0).take(256*3).collect();
                    pcx_reader.read_palette(&mut palette)?;
                }

                (pixels, width as u16, height as u16)
            };

            if !load_raw_pcx {
                pcx.clear();
                pcx.shrink_to_fit();
            }

            images.insert(name, Image {
                info : info,
                width : width,
                height : height,
                pixels : pixels,
                pcx : pcx,
            });
        }

        Ok(Lgr {
            images : images,
            palette : palette,
        })
    }
}

fn trim_at_zero(string : &mut Vec<u8>) {
    for i in 0..string.len() {
        if string[i] == 0 {
            string.truncate(i);
            break;
        }
    }
}

fn error<T>(msg : &str) -> io::Result<T> {
    Err(io::Error::new(io::ErrorKind::InvalidData, msg))
}

fn read_string<R : io::Read>(stream : &mut R, len : usize) -> io::Result<String> {
    let mut string : Vec<u8> = std::iter::repeat(0).take(len).collect();
    stream.read_exact(&mut string)?;

    trim_at_zero(&mut string);

    match String::from_utf8(string) {
        Ok(s) => Ok(s),
        Err(_) => error("LGR: invalid ASCII"),
    }
}

fn read_pictures<R : io::Read>(stream : &mut R, listed_images : usize) -> io::Result<BTreeMap<String, Picture>> {
    // need tp init array to something, these values will be overwritten
    let initial_info = Picture {
        kind : PictureKind::Picture,
        distance : 0,
        clipping : ClippingMode::Unclipped,
    };
    let mut pictures : Vec<(String, Picture)> = std::iter::repeat(("".to_string(), initial_info)).take(listed_images).collect();

    for i in 0..listed_images {
        pictures[i].0 = read_string(stream, 10)?;
    }

    for i in 0..listed_images {
        let kind = stream.read_u32::<LittleEndian>()?;
        pictures[i].1.kind = match kind {
            100 => PictureKind::Picture,
            101 => PictureKind::Texture,
            102 => PictureKind::Mask,
            _ => return error("LGR: unknown picture kind"),
        };
    }

    for i in 0..listed_images {
        pictures[i].1.distance = stream.read_u32::<LittleEndian>()?;
    }

    for i in 0..listed_images {
        let clipping = stream.read_u32::<LittleEndian>()?;
        pictures[i].1.clipping = match clipping {
            0 => ClippingMode::Unclipped,
            1 => ClippingMode::Ground,
            2 => ClippingMode::Sky,
            _ => return error("LGR: unknown clipping mode"),
        };
    }

    for _ in 0..listed_images {
        let unknown = stream.read_u32::<LittleEndian>()?;
        if unknown != 12 {
            return error("LGR: invalid unknown value != 12");
        }
    }

    Ok(pictures.into_iter().collect())
}

#[cfg(test)]
mod tests {
    use Lgr;

    #[test]
    fn load() {
        let lgr = Lgr::load_from_file("lgr/example.lgr", false, false).unwrap();
        assert_eq!(lgr.images.len(), 77);

        let lgr = Lgr::load_from_file("lgr/example.lgr", false, true).unwrap();
        assert_eq!(lgr.images.len(), 77);

        let lgr = Lgr::load_from_file("lgr/example.lgr", true, false).unwrap();
        assert_eq!(lgr.images.len(), 77);

        let lgr = Lgr::load_from_file("lgr/example.lgr", true, true).unwrap();
        assert_eq!(lgr.images.len(), 77);
    }
}