Episode 9: Build Data Structure

Julien Truffaut

21st 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.

© 2026 RustJobs.dev, All rights reserved.