Episode 10: Searching Through Gear Combinations

Julien Truffaut image

Julien Truffaut

27 November 2025

In the previous episode, I introduced the Build data structure, which stores gears in the appropriate slot (hat, belt, weapon, …).

Today, I’m finally implementing the simplest possible search algorithm: iterating over every combination of gear.

Preparing the Dataset

Before searching, I need all available Gear grouped by slot.

I already have a helper function in the core crate to get me started:

use dofus_opti_core::{Gear, GearType};

let amulets = import_gears(&[GearType::Amulet])?;
let belts   = import_gears(&[GearType::Belt])?;
...

fn import_gears(gear_types: &[GearType]) -> anyhow::Result<Vec<Gear>> {
    let mut all_gears: Vec<Gear> = Vec::new();

    for gear_type in gear_types {
        let mut gears = dofus_opti_core::file::read_gears("core/data", gear_type)?;
        all_gears.append(&mut gears);
    }

    Ok(all_gears)
}

I use a slice (&[GearType]) so I can load multiple gear types for the weapon slot:

let weapon_gear_types = &[
    GearType::Axe,
    GearType::Bow,
    GearType::Dagger,
    // ...
];

let weapons = import_gears(weapon_gear_types)?;

Nothing fancy so far.

As discussed earlier, the simplest search algorithm is just… nested loops:

let mut build = Build::new();

for amulet in amulets {
    build.set_gear(GearSlot::Amulet, amulet)?;
    for belt in belts {
        build.set_gear(GearSlot::Belt, belt)?;
        ...
    }
}

But, this doesn’t compile:

`belts` moved due to this implicit call to `.into_iter()`

I learned here that Rust’s for loop actually desugars to something like:

let mut iter = (&amulets).into_iter();
while let Some(amulet) = iter.next() {
    ...
}

Calling .into_iter() moves the values out of the vector, consuming it.

That’s why after a for loop, you can’t reuse the vector:

for amulet in amulets { ... }
println!("{}", amulets.len()); // ❌ amulets is moved

amulets is gone.

And in nested loops, the inner vectors need to be iterated many times — which is impossible if they were consumed in the first iteration.

Solution: iterate by reference

for amulet in &amulets {
    build.set_gear(GearSlot::Amulet, amulet)?;
    for belt in &belts {
        build.set_gear(GearSlot::Belt, belt)?;
        ...
    }
}

Now the vectors are borrowed, not moved.

…and we hit another problem

expected `Gear`, found `&Gear`

We borrowed each gear as &Gear, but set_gear expects owned Gear.

There are two options:

  1. Clone the gear. It works, but is inefficient — we would duplicate thousands of Gear values.
  2. Make Build store &Gear instead. This avoids unnecessary cloning and matches our use-case better.

Let’s go with (2).

Updating Build to Store Gear References

Let’s modify the struct to store references:

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

The compiler now complains:

expected named lifetime parameter

Because whenever you store references in a struct, you must specify how long they live using lifetime parameters:

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

This means: “Build<'a> cannot outlive the gears it points to.”

Update the impl block accordingly:

We also need to add a lifetime parameter in the impl block:

impl<'a> Build<'a> {
    fn new() -> Self {
        Self { gear_slots: HashMap::new() }
    }

    fn get_gear(&self, slot: &GearSlot) -> Option<&'a Gear> {
        self.gear_slots.get(slot).map(|g| *g)
    }

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

Note that I needed to add a map(|g| *g) in get_gear because HashMap::get returns a reference toward the value of map, so in this case, it gives a &&Gear (a pointer toward a pointer of a Gear). I could also use copied:

self.gear_slots.get(slot).copied()

Wrapping it Up

To visually validate that the nested loops behave as expected, I implemented a small summary method on Build and printed each generated configuration:

for amulet in &amulets {
    build.set_gear(GearSlot::Amulet, amulet)?;
    for belt in &belts {
        build.set_gear(GearSlot::Belt, belt)?;
        // ...
        for weapon in &weapons {
            build.set_gear(GearSlot::Weapon, weapon)?;
            println!("{}", build.summary());
        }
    }
}

And sure enough, the program produces an endless stream of build combinations, such as:

Build {
    amulet: Gelamu,
    belt: Doggona Rope,
    boots: Veggie Boots,
    hat: Arpone Mask,
    ring 1: Vicious Ring,
    ring 2: Vicious Ring,
    shield: Trophy Moon Shield,
    weapon: Purrin Axe,
}

Build {
    amulet: Gelamu,
    belt: Doggona Rope,
    boots: Veggie Boots,
    hat: Arpone Mask,
    ring 1: Vicious Ring,
    ring 2: Vicious Ring,
    shield: Trophy Moon Shield,
    weapon: Lookabeer Axe,
}

...

This confirms that the basic search mechanism is working — even if it’s comically inefficient at this stage!

Conclusion

This was my first real encounter with references and lifetimes in Rust.

It took this long because everything I’d built so far was a data pipeline: read JSON → convert into structs → write JSON. Each step moves ownership along, so lifetimes never mattered.

But as soon as you try to share data — like referencing gears inside a build — Rust makes sure everything is safe.

Another thing I learned: when in doubt, you can always .clone() something to sidestep ownership issues. It’s inefficient, but for a beginner (or when prototyping) it’s an invaluable escape hatch.

Next time, I’ll look into whether this brute-force nested-loop approach is viable, or whether I need something more clever.