Polymorphism for reading Source BSP data


#1

Hello,

I’m currently playing around with Rust trying to read Source BSP files. The basic structure of these files is that we have “lumps” of data, which are basically arrays of structs.

An example of such a struct is:

pub struct Vector {
    x: f32,
    y: f32,
    z: f32,
}

and I am using this block of code to load all Vectors from a file:

pub fn load_vertices<R: BufRead + Seek, O: ByteOrder>(
        &self,
        reader: &mut R,
    ) -> Option<Vec<Vector>> {

        /*let lump = self.header.lumps[3];*/
        let offset = lump.offset;
        let len = lump.length;

        if offset != -1 && len != -1 {
            
            /*let vert_size = std::mem::size_of::<Vector>();
            let count = len as usize / vert_size;
            let mut v: Vec<Vector> = Vec::with_capacity(count);*/

            for i in 0..count {

                /*let x = reader.read_f32::<O>().unwrap();
                let y = reader.read_f32::<O>().unwrap();
                let z = reader.read_f32::<O>().unwrap();

                let vertex = Vector::new(x, y, z);
                v[i] = vertex;*/
            }
            Some(v)
        } else {
            None
        }
    }

So far so good. Now there are other data types in the file, such as Plane.
How can I generalize the commented-out blocks of the load_vertices method to load other types? I know that I probably need runtime polymorphism, but how would I achieve that in Rust?


#2

It appears like a closed set of types, so you could probably model the lump as an enum. Something like:

enum Lump {
   Vectors(Vec<Vector>),
   Planes(Vec<Plane>),
   ...
}

fn load_lump<R: BufRead + Seek, O: ByteOrder>(
        &self,
        reader: &mut R,
    ) -> Option<Lump> {
    // determine the type of lump and then deserialize accordingly
}

Another option is to do something like this (rough sketch):

trait LumpItem {
    fn deser<R: BufRead + Seek>(r: &mut R) -> Self; // maybe Result<Self, SomeError>?
}

struct Vector { ... }
impl Deserializable for Vector { 
    fn deser<R: BufRead + Seek>(r: &mut R) -> Self {
       // read in the x, y, and z values, and return a Vector
    }
}
struct Plane { ... }
impl Deserializable for Plane { ... }

fn load_lump<R: BufRead + Seek, O: ByteOrder, L: LumpItem>(&self, reader: &mut R) -> Option<Vec<L>> {
        let offset = lump.offset;
        let len = lump.length;

        if offset != -1 && len != -1 {
            
            let size = std::mem::size_of::<L>();
            let count = len as usize / size;
            let mut v: Vec<L> = Vec::with_capacity(count);

            for i in 0..count {
               v.push(L::deser(reader));
            }
            Some(v)
        } else {
            None
        }
}

One thing to be mindful is if std::mem::size_of will return the correct size as used in the file. You’ll need to make sure your Rust struct size matches exactly what the file contains; you may want to consider using repr(C) for that.


#3

First of all, thanks a lot for responding. :slight_smile:

About the first suggestion, I thought about using enums because they seemed to fit the case. However, I’d like the user to choose which lump he wants to load by passing the enum into the function and having to provide a Vec<T> when creating a new instance seemed a bit unwieldy to me.

I like the second approach you presented and I’m going to try and implement it that way. Also, thanks for the tip about repr(C).