Episode 9: Build Data Structure

Julien Truffaut image

Julien Truffaut

21 November 2025

In the previous episode, I reorganised the project into three crates: core, dofus_db, and build. The last one will be the new playground where I’ll develop the gear–selection optimisation tool.

Today’s task: designing the Build data structure.

Defining a Build

A build needs to store gears in specific positions: a hat, a shield, a weapon, etc. We need a type that represents these gear slots.

It might be tempting to reuse the GearType enum from the core crate, but the mapping isn’t 1-to-1:

  • A Hat can only go in the Hat slot ✔
  • But the Weapon slot can accept axes, swords, bows, hammers, etc.
  • And there are two ring slots.

The simplest approach is a dedicated enum:

enum GearSlot {
    Amulet,
    Belt,
    Boots,
    Cloak,
    Hat,
    Ring1,
    Ring2,
    Shield,
    Weapon,
}

A build is then a map from slot → gear:

use std::collections::HashMap;

struct Build {
    gear_slots: HashMap<GearSlot, Gear>,
}

I prefer not to expose gear_slots directly. Having accessors gives flexibility to change the internals later.

impl Build {
    fn get_gear(&self, gear_slot: &GearSlot) -> Option<&Gear> {
        self.gear_slots.get(gear_slot)
    }

    fn set_gear(&mut self, gear_slot: GearSlot, gear: Gear) {
        self.gear_slots.insert(gear_slot, gear);
    }
}

So far so good… until the compiler complains:

enum GearSlot {
     ^^^ doesn't satisfy `GearSlot: Hash` or `GearSlot: Eq`

self.gear_slots.get(&gear_slot)
                    ^^^ method cannot be called on `HashMap<GearSlot, Gear>` due to unsatisfied trait bounds

As it often the case in rust, the compiler gives a very helpful suggestion:

help: consider annotating `GearSlot` with `#[derive(Eq, Hash, PartialEq)]`

Structs and enums don’t automatically implement Eq or Hash (unlike JVM languages like Java or Scala). But adding these trait using derives is trivial:

#[derive(Eq, Hash, PartialEq)]
enum GearSlot { ... }

Validation

The above implementation of set_gear is incorrect because it allows inserting any gear into any slot—for example a ring into a hat slot.

We need validation logic:

impl GearSlot {
    pub fn is_valid_for(&self, gear_type: &GearType) -> bool {
        match (self, gear_type) {
            (GearSlot::Amulet, GearType::Amulet) => true,
            (GearSlot::Belt,   GearType::Belt)   => true,
            (GearSlot::Boots,  GearType::Boots)  => true,
            (GearSlot::Cloak,  GearType::Cloak)  => true,
            (GearSlot::Hat,    GearType::Hat)    => true,
            (GearSlot::Ring1,  GearType::Ring)   => true,
            (GearSlot::Ring2,  GearType::Ring)   => true,
            (GearSlot::Shield, GearType::Shield) => true,
            (GearSlot::Weapon, GearType::Axe)    => true,
            (GearSlot::Weapon, GearType::Bow)    => true,
            (GearSlot::Weapon, GearType::Dagger) => true,
            // ... all other weapon types ...
            _ => false,
        }
    }
}

fn check_gear_slot(gear: &Gear, gear_slot: &GearSlot) -> Result<(), String> {
    if gear_slot.is_valid_for(&gear.gear_type) {
        Ok(())
    } else {
        Err(format!(
            "Gear cannot be put in the expected slot, gear: {}, slot: {}",
            gear.name.en, gear_slot
        ))
    }
}

Now we can update set_gear to propagate errors using ?:

fn set_gear(&mut self, gear_slot: GearSlot, gear: Gear) -> Result<(), String> {
    check_gear_slot(&gear, &gear_slot)?;
    self.gear_slots.insert(gear_slot, gear);
    Ok(())
}

But the compiler again gives an error because gear_slot variable was moved into check_gear_slot and then reused afterward:

error[E0382]: use of moved value: `gear_slot`

check_gear_slot(&gear, gear_slot)?;
                       ^^^^^^^^^ value moved here
self.gear_slots.insert(gear_slot, gear);
                       ^^^^^^^^^ value used here after move

Since GearSlot is tiny, we can cheaply copy it. Again using the Copy and Clone derivation:

#[derive(Eq, Hash, PartialEq, Clone, Copy)]
enum GearSlot { ... }

And if you don’t trust me that GearSlot is small, you can ask rust how much memory it takes:

println!("GearSlot takes {} Bytes", std::mem::size_of::<GearSlot>());
// GearSlot takes 1 Bytes

Custom Error Type

Returning a String as an error isn’t great both for testing or writing error handling logic. Let’s define a dedicated error type instead:

enum BuildError {
    InvalidGearSlot(String, GearSlot),
}

To integrate with Rust’s error ecosystem we need to implement:

  • std::fmt::Display
  • std::fmt::Debug
  • std::error::Error

Rather than implementing these traits manually, we can use thiserror crate:

[dependencies]
thiserror = "1.0"
use thiserror::Error;

#[derive(Debug, Error, PartialEq, Eq)]
pub enum BuildError {
    #[error("Gear cannot be put in the expected slot, gear: {0}, slot: {1}")]
    InvalidGearSlot(String, GearSlot),
}

Much cleaner!

Conclusion

We now have the foundation of the Build data structure with:

  • a GearSlot enum tailored to the build problem
  • safe get_gear and set_gear functions
  • the foundation for most advanced validation using the BuildError enum

In the next episode, I’ll implement the “dumb” brute-force build search and test whether the Build data structure holds up.