diff --git a/laevis/CHANGELOG.md b/laevis/CHANGELOG.md
index e96d2b6..f011538 100644
--- a/laevis/CHANGELOG.md
+++ b/laevis/CHANGELOG.md
@@ -7,6 +7,9 @@
 - Change: Explosive Death's range now increases more slowly with levels, and is
   based on the radius of the exploding monster; bigger enemies produce bigger booms.
 - Change: split the menu code into a separate library, libtooltipmenu.pk3
+- Change: you can now pick between 4 upgrades when you gain a level
+- Fix: upgrade generation can no longer take unbounded time if you're unlucky
+- Fix: upgrade generation can no longer freeze the game if the pool of upgrade candidates is very small
 - Fix: Added some missing sprites to the repo (they were still in the pk3 but not versioned)
 
 # 0.6.4
diff --git a/laevis/ca.ancilla.laevis/PlayerUpgradeGiver.zs b/laevis/ca.ancilla.laevis/PlayerUpgradeGiver.zs
index 4c2fb62..7305cfd 100644
--- a/laevis/ca.ancilla.laevis/PlayerUpgradeGiver.zs
+++ b/laevis/ca.ancilla.laevis/PlayerUpgradeGiver.zs
@@ -5,17 +5,9 @@
 class ::PlayerUpgradeGiver : ::UpgradeGiver {
   ::PerPlayerStats stats;
 
-  // TODO: we might want to force the first upgrade to always be a damage bonus
-  // or some other simple, generally useful upgrade.
   override void CreateUpgradeCandidates() {
-    while (candidates.size() < 3) {
-      let upgrade = ::Upgrade::Registry.GenerateUpgradeForPlayer(stats);
-      if (!AlreadyHasUpgrade(upgrade)) {
-        candidates.push(upgrade);
-      } else {
-        upgrade.Destroy();
-      }
-    }
+    candidates.clear();
+    ::Upgrade::Registry.GenerateUpgradesForPlayer(stats, candidates);
   }
 
   void InstallUpgrade(int index) {
@@ -23,7 +15,7 @@ class ::PlayerUpgradeGiver : ::UpgradeGiver {
       console.printf("Level-up rejected!");
     } else {
       console.printf("You gained a level of %s!", candidates[index].GetName());
-      stats.upgrades.AddUpgrade(candidates[index]);
+      stats.upgrades.Add(candidates[index].GetClassName());
     }
     Destroy();
   }
diff --git a/laevis/ca.ancilla.laevis/WeaponUpgradeGiver.zs b/laevis/ca.ancilla.laevis/WeaponUpgradeGiver.zs
index 4925f34..97ef421 100644
--- a/laevis/ca.ancilla.laevis/WeaponUpgradeGiver.zs
+++ b/laevis/ca.ancilla.laevis/WeaponUpgradeGiver.zs
@@ -6,16 +6,8 @@ class ::WeaponUpgradeGiver : ::UpgradeGiver {
   TFLV_WeaponInfo wielded;
 
   override void CreateUpgradeCandidates() {
-    // TODO: properly handle the case where the number of valid upgrades is
-    // less than the number we want to display.
-    while (candidates.size() < 3) {
-      let upgrade = ::Upgrade::Registry.GenerateUpgradeForWeapon(wielded);
-      if (!AlreadyHasUpgrade(upgrade)) {
-        candidates.push(upgrade);
-      } else {
-        upgrade.Destroy();
-      }
-    }
+    candidates.clear();
+    ::Upgrade::Registry.GenerateUpgradesForWeapon(wielded, candidates);
   }
 
   void InstallUpgrade(int index) {
@@ -24,7 +16,7 @@ class ::WeaponUpgradeGiver : ::UpgradeGiver {
     } else {
       console.printf("Your %s gained a level of %s!",
         wielded.weapon.GetTag(), candidates[index].GetName());
-      wielded.upgrades.AddUpgrade(candidates[index]);
+      wielded.upgrades.Add(candidates[index].GetClassName());
     }
     Destroy();
   }
diff --git a/laevis/ca.ancilla.laevis/upgrades/Registry.zs b/laevis/ca.ancilla.laevis/upgrades/Registry.zs
index fa4ce9c..f1c84ce 100644
--- a/laevis/ca.ancilla.laevis/upgrades/Registry.zs
+++ b/laevis/ca.ancilla.laevis/upgrades/Registry.zs
@@ -1,8 +1,11 @@
 #namespace TFLV::Upgrade;
 #debug off
 
+const UPGRADES_PER_LEVEL = 4;
+
 class ::Registry : Object play {
-  array<string> upgrades;
+  array<string> upgrade_names;
+  array<::BaseUpgrade> upgrades;
 
   static ::Registry GetRegistry() {
     let reg = TFLV::EventHandler(StaticEventHandler.Find("TFLV::EventHandler"));
@@ -13,13 +16,14 @@ class ::Registry : Object play {
 
   static void Register(string upgrade) {
     DEBUG("Register: %s", upgrade);
-    if (GetRegistry().upgrades.find(upgrade) != GetRegistry().upgrades.size()) {
+    if (GetRegistry().upgrade_names.find(upgrade) != GetRegistry().upgrade_names.size()) {
       // Assume that this is because a mod has tried to double-register an upgrade,
       // and permit it as a no-op.
       //ThrowAbortException("Duplicate upgrades named %s", upgrade);
       return;
     }
-    GetRegistry().upgrades.push(upgrade);
+    GetRegistry().upgrade_names.push(upgrade);
+    GetRegistry().upgrades.push(::BaseUpgrade(new(upgrade)));
   }
 
   // Can't be static because we need to call it during eventmanager initialization,
@@ -63,35 +67,40 @@ class ::Registry : Object play {
       "::Embrittlement"
     };
     for (uint i = 0; i < UpgradeNames.size(); ++i) {
-      upgrades.push(UpgradeNames[i]);
+      upgrade_names.push(UpgradeNames[i]);
+      upgrades.push(::BaseUpgrade(new(UpgradeNames[i])));
     }
   }
 
-  static ::BaseUpgrade GenerateUpgrade() {
-    let cls = GetRegistry().upgrades[random(0, GetRegistry().upgrades.size()-1)];
-    DEBUG("GenerateUpgrade(%s)", cls);
-    return ::BaseUpgrade(new(cls));
+  static void PickN(Array<::BaseUpgrade> dst, Array<::BaseUpgrade> src, uint n) {
+    uint max = src.size();
+    while (max > 0 && dst.size() < n) {
+      uint i = random(0, max-1);
+      dst.push(src[i]);
+      src[i] = src[--max];
+    }
   }
 
-  static ::BaseUpgrade GenerateUpgradeForPlayer(TFLV::PerPlayerStats stats) {
-    ::BaseUpgrade upgrade = null;
-    while (upgrade == null) {
-      upgrade = ::Registry.GenerateUpgrade();
-      if (upgrade.IsSuitableForPlayer(stats)) return upgrade;
-      upgrade.Destroy();
-      upgrade = null;
+  static void GenerateUpgradesForPlayer(
+      TFLV::PerPlayerStats stats, Array<::BaseUpgrade> generated) {
+    Array<::BaseUpgrade> candidates;
+    // Array<::BaseUpgrade> all_upgrades = GetRegistry().upgrades;
+    for (uint i = 0; i < GetRegistry().upgrades.size(); ++i) {
+      if (GetRegistry().upgrades[i].IsSuitableForPlayer(stats))
+        candidates.push(GetRegistry().upgrades[i]);
     }
-    return null; // unreachable
+
+    PickN(generated, candidates, UPGRADES_PER_LEVEL);
   }
 
-  static ::BaseUpgrade GenerateUpgradeForWeapon(TFLV::WeaponInfo info) {
-    ::BaseUpgrade upgrade = null;
-    while (upgrade == null) {
-      upgrade = ::Registry.GenerateUpgrade();
-      if (upgrade.IsSuitableForWeapon(info)) return upgrade;
-      upgrade.Destroy();
-      upgrade = null;
+  static void GenerateUpgradesForWeapon(
+      TFLV::WeaponInfo info, Array<::BaseUpgrade> generated) {
+    array<::BaseUpgrade> candidates;
+    for (uint i = 0; i < GetRegistry().upgrades.size(); ++i) {
+      if (GetRegistry().upgrades[i].IsSuitableForWeapon(info))
+        candidates.push(GetRegistry().upgrades[i]);
     }
-    return null; // unreachable
+
+    PickN(generated, candidates, UPGRADES_PER_LEVEL);
   }
 }