diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py
index 38be2cd794a1..e2602036a24e 100644
--- a/worlds/hk/Options.py
+++ b/worlds/hk/Options.py
@@ -1,10 +1,12 @@
 import typing
 import re
+from dataclasses import dataclass, make_dataclass
+
 from .ExtractedData import logic_options, starts, pool_options
 from .Rules import cost_terms
 from schema import And, Schema, Optional
 
-from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink
+from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink, PerGameCommonOptions
 from .Charms import vanilla_costs, names as charm_names
 
 if typing.TYPE_CHECKING:
@@ -538,3 +540,5 @@ class CostSanityHybridChance(Range):
     },
     **cost_sanity_weights
 }
+
+HKOptions = make_dataclass("HKOptions", [(name, option) for name, option in hollow_knight_options.items()], bases=(PerGameCommonOptions,))
diff --git a/worlds/hk/Rules.py b/worlds/hk/Rules.py
index a3c7e13cf02b..e162e1dfa81c 100644
--- a/worlds/hk/Rules.py
+++ b/worlds/hk/Rules.py
@@ -49,3 +49,42 @@ def set_rules(hk_world: World):
                 if term == "GEO":  # No geo logic!
                     continue
                 add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount)
+
+
+def _hk_nail_combat(state, player) -> bool:
+    return state.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player)
+
+
+def _hk_can_beat_thk(state, player) -> bool:
+    return (
+        state.has('Opened_Black_Egg_Temple', player)
+        and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1
+        and _hk_nail_combat(state, player)
+        and (
+            state.has_any({'LEFTDASH', 'RIGHTDASH'}, player)
+            or state._hk_option(player, 'ProficientCombat')
+        )
+        and state.has('FOCUS', player)
+    )
+
+
+def _hk_siblings_ending(state, player) -> bool:
+    return _hk_can_beat_thk(state, player) and state.has('WHITEFRAGMENT', player, 3)
+
+
+def _hk_can_beat_radiance(state, player) -> bool:
+    return (
+        state.has('Opened_Black_Egg_Temple', player)
+        and _hk_nail_combat(state, player)
+        and state.has('WHITEFRAGMENT', player, 3)
+        and state.has('DREAMNAIL', player)
+        and (
+            (state.has('LEFTCLAW', player) and state.has('RIGHTCLAW', player))
+            or state.has('WINGS', player)
+        )
+        and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1
+        and (
+            (state.has('LEFTDASH', player, 2) and state.has('RIGHTDASH', player, 2))  # Both Shade Cloaks
+            or (state._hk_option(player, 'ProficientCombat') and state.has('QUAKE', player))  # or Dive
+        )
+    )
diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py
index fbc6461f6aab..e5065876ddf3 100644
--- a/worlds/hk/__init__.py
+++ b/worlds/hk/__init__.py
@@ -10,9 +10,9 @@
 
 from .Items import item_table, lookup_type_to_names, item_name_groups
 from .Regions import create_regions
-from .Rules import set_rules, cost_terms
+from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance
 from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \
-    shop_to_option
+    shop_to_option, HKOptions
 from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \
     event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs
 from .Charms import names as charm_names
@@ -142,7 +142,8 @@ class HKWorld(World):
     As the enigmatic Knight, you’ll traverse the depths, unravel its mysteries and conquer its evils.
     """  # from https://www.hollowknight.com
     game: str = "Hollow Knight"
-    option_definitions = hollow_knight_options
+    options_dataclass = HKOptions
+    options: HKOptions
 
     web = HKWeb()
 
@@ -155,8 +156,8 @@ class HKWorld(World):
     charm_costs: typing.List[int]
     cached_filler_items = {}
 
-    def __init__(self, world, player):
-        super(HKWorld, self).__init__(world, player)
+    def __init__(self, multiworld, player):
+        super(HKWorld, self).__init__(multiworld, player)
         self.created_multi_locations: typing.Dict[str, typing.List[HKLocation]] = {
             location: list() for location in multi_locations
         }
@@ -165,29 +166,29 @@ def __init__(self, world, player):
         self.vanilla_shop_costs = deepcopy(vanilla_shop_costs)
 
     def generate_early(self):
-        world = self.multiworld
-        charm_costs = world.RandomCharmCosts[self.player].get_costs(world.random)
-        self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs)
-        # world.exclude_locations[self.player].value.update(white_palace_locations)
+        options = self.options
+        charm_costs = options.RandomCharmCosts.get_costs(self.random)
+        self.charm_costs = options.PlandoCharmCosts.get_costs(charm_costs)
+        # options.exclude_locations.value.update(white_palace_locations)
         for term, data in cost_terms.items():
-            mini = getattr(world, f"Minimum{data.option}Price")[self.player]
-            maxi = getattr(world, f"Maximum{data.option}Price")[self.player]
+            mini = getattr(options, f"Minimum{data.option}Price")
+            maxi = getattr(options, f"Maximum{data.option}Price")
             # if minimum > maximum, set minimum to maximum
             mini.value = min(mini.value, maxi.value)
             self.ranges[term] = mini.value, maxi.value
-        world.push_precollected(HKItem(starts[world.StartLocation[self.player].current_key],
+        self.multiworld.push_precollected(HKItem(starts[options.StartLocation.current_key],
                                        True, None, "Event", self.player))
 
     def white_palace_exclusions(self):
         exclusions = set()
-        wp = self.multiworld.WhitePalace[self.player]
+        wp = self.options.WhitePalace
         if wp <= WhitePalace.option_nopathofpain:
             exclusions.update(path_of_pain_locations)
         if wp <= WhitePalace.option_kingfragment:
             exclusions.update(white_palace_checks)
         if wp == WhitePalace.option_exclude:
             exclusions.add("King_Fragment")
-            if self.multiworld.RandomizeCharms[self.player]:
+            if self.options.RandomizeCharms:
                 # If charms are randomized, this will be junk-filled -- so transitions and events are not progression
                 exclusions.update(white_palace_transitions)
                 exclusions.update(white_palace_events)
@@ -200,7 +201,7 @@ def create_regions(self):
 
         # check for any goal that godhome events are relevant to
         all_event_names = event_names.copy()
-        if self.multiworld.Goal[self.player] in [Goal.option_godhome, Goal.option_godhome_flower]:
+        if self.options.Goal in [Goal.option_godhome, Goal.option_godhome_flower]:
             from .GodhomeData import godhome_event_names
             all_event_names.update(set(godhome_event_names))
 
@@ -230,12 +231,12 @@ def create_items(self):
         pool: typing.List[HKItem] = []
         wp_exclusions = self.white_palace_exclusions()
         junk_replace: typing.Set[str] = set()
-        if self.multiworld.RemoveSpellUpgrades[self.player]:
+        if self.options.RemoveSpellUpgrades:
             junk_replace.update(("Abyss_Shriek", "Shade_Soul", "Descending_Dark"))
 
         randomized_starting_items = set()
         for attr, items in randomizable_starting_items.items():
-            if getattr(self.multiworld, attr)[self.player]:
+            if getattr(self.options, attr):
                 randomized_starting_items.update(items)
 
         # noinspection PyShadowingNames
@@ -257,7 +258,7 @@ def _add(item_name: str, location_name: str, randomized: bool):
             if item_name in junk_replace:
                 item_name = self.get_filler_item_name()
 
-            item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.multiworld.AddUnshuffledLocations[self.player] else self.create_event(item_name)
+            item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations else self.create_event(item_name)
 
             if location_name == "Start":
                 if item_name in randomized_starting_items:
@@ -281,55 +282,55 @@ def _add(item_name: str, location_name: str, randomized: bool):
                 location.progress_type = LocationProgressType.EXCLUDED
 
         for option_key, option in hollow_knight_randomize_options.items():
-            randomized = getattr(self.multiworld, option_key)[self.player]
-            if all([not randomized, option_key in logicless_options, not self.multiworld.AddUnshuffledLocations[self.player]]):
+            randomized = getattr(self.options, option_key)
+            if all([not randomized, option_key in logicless_options, not self.options.AddUnshuffledLocations]):
                 continue
             for item_name, location_name in zip(option.items, option.locations):
                 if item_name in junk_replace:
                     item_name = self.get_filler_item_name()
 
-                if (item_name == "Crystal_Heart" and self.multiworld.SplitCrystalHeart[self.player]) or \
-                        (item_name == "Mothwing_Cloak" and self.multiworld.SplitMothwingCloak[self.player]):
+                if (item_name == "Crystal_Heart" and self.options.SplitCrystalHeart) or \
+                        (item_name == "Mothwing_Cloak" and self.options.SplitMothwingCloak):
                     _add("Left_" + item_name, location_name, randomized)
                     _add("Right_" + item_name, "Split_" + location_name, randomized)
                     continue
-                if item_name == "Mantis_Claw" and self.multiworld.SplitMantisClaw[self.player]:
+                if item_name == "Mantis_Claw" and self.options.SplitMantisClaw:
                     _add("Left_" + item_name, "Left_" + location_name, randomized)
                     _add("Right_" + item_name, "Right_" + location_name, randomized)
                     continue
-                if item_name == "Shade_Cloak" and self.multiworld.SplitMothwingCloak[self.player]:
-                    if self.multiworld.random.randint(0, 1):
+                if item_name == "Shade_Cloak" and self.options.SplitMothwingCloak:
+                    if self.random.randint(0, 1):
                         item_name = "Left_Mothwing_Cloak"
                     else:
                         item_name = "Right_Mothwing_Cloak"
-                if item_name == "Grimmchild2" and self.multiworld.RandomizeGrimmkinFlames[self.player] and self.multiworld.RandomizeCharms[self.player]:
+                if item_name == "Grimmchild2" and self.options.RandomizeGrimmkinFlames and self.options.RandomizeCharms:
                     _add("Grimmchild1", location_name, randomized)
                     continue
 
                 _add(item_name, location_name, randomized)
 
-        if self.multiworld.RandomizeElevatorPass[self.player]:
+        if self.options.RandomizeElevatorPass:
             randomized = True
             _add("Elevator_Pass", "Elevator_Pass", randomized)
 
         for shop, locations in self.created_multi_locations.items():
-            for _ in range(len(locations), getattr(self.multiworld, shop_to_option[shop])[self.player].value):
+            for _ in range(len(locations), getattr(self.options, shop_to_option[shop]).value):
                 loc = self.create_location(shop)
                 unfilled_locations += 1
 
         # Balance the pool
         item_count = len(pool)
-        additional_shop_items = max(item_count - unfilled_locations, self.multiworld.ExtraShopSlots[self.player].value)
+        additional_shop_items = max(item_count - unfilled_locations, self.options.ExtraShopSlots.value)
 
         # Add additional shop items, as needed.
         if additional_shop_items > 0:
             shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16)
-            if not self.multiworld.EggShopSlots[self.player].value:  # No eggshop, so don't place items there
+            if not self.options.EggShopSlots:  # No eggshop, so don't place items there
                 shops.remove('Egg_Shop')
 
             if shops:
                 for _ in range(additional_shop_items):
-                    shop = self.multiworld.random.choice(shops)
+                    shop = self.random.choice(shops)
                     loc = self.create_location(shop)
                     unfilled_locations += 1
                     if len(self.created_multi_locations[shop]) >= 16:
@@ -355,7 +356,7 @@ def sort_shops_by_cost(self):
                 loc.costs = costs
 
     def apply_costsanity(self):
-        setting = self.multiworld.CostSanity[self.player].value
+        setting = self.options.CostSanity.value
         if not setting:
             return  # noop
 
@@ -369,10 +370,10 @@ def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]:
 
             return {k: v for k, v in weights.items() if v}
 
-        random = self.multiworld.random
-        hybrid_chance = getattr(self.multiworld, f"CostSanityHybridChance")[self.player].value
+        random = self.random
+        hybrid_chance = getattr(self.options, f"CostSanityHybridChance").value
         weights = {
-            data.term: getattr(self.multiworld, f"CostSanity{data.option}Weight")[self.player].value
+            data.term: getattr(self.options, f"CostSanity{data.option}Weight").value
             for data in cost_terms.values()
         }
         weights_geoless = dict(weights)
@@ -427,22 +428,22 @@ def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]:
                 location.sort_costs()
 
     def set_rules(self):
-        world = self.multiworld
+        multiworld = self.multiworld
         player = self.player
-        goal = world.Goal[player]
+        goal = self.options.Goal
         if goal == Goal.option_hollowknight:
-            world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player)
+            multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player)
         elif goal == Goal.option_siblings:
-            world.completion_condition[player] = lambda state: state._hk_siblings_ending(player)
+            multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player)
         elif goal == Goal.option_radiance:
-            world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player)
+            multiworld.completion_condition[player] = lambda state: _hk_can_beat_radiance(state, player)
         elif goal == Goal.option_godhome:
-            world.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player)
+            multiworld.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player)
         elif goal == Goal.option_godhome_flower:
-            world.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player)
+            multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player)
         else:
             # Any goal
-            world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player)
+            multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) or _hk_can_beat_radiance(state, player)
 
         set_rules(self)
 
@@ -450,8 +451,8 @@ def fill_slot_data(self):
         slot_data = {}
 
         options = slot_data["options"] = {}
-        for option_name in self.option_definitions:
-            option = getattr(self.multiworld, option_name)[self.player]
+        for option_name in hollow_knight_options:
+            option = getattr(self.options, option_name)
             try:
                 optionvalue = int(option.value)
             except TypeError:
@@ -460,10 +461,10 @@ def fill_slot_data(self):
                 options[option_name] = optionvalue
 
         # 32 bit int
-        slot_data["seed"] = self.multiworld.per_slot_randoms[self.player].randint(-2147483647, 2147483646)
+        slot_data["seed"] = self.random.randint(-2147483647, 2147483646)
 
         # Backwards compatibility for shop cost data (HKAP < 0.1.0)
-        if not self.multiworld.CostSanity[self.player]:
+        if not self.options.CostSanity:
             for shop, terms in shop_cost_types.items():
                 unit = cost_terms[next(iter(terms))].option
                 if unit == "Geo":
@@ -498,7 +499,7 @@ def create_location(self, name: str, vanilla=False) -> HKLocation:
         basename = name
         if name in shop_cost_types:
             costs = {
-                term: self.multiworld.random.randint(*self.ranges[term])
+                term: self.random.randint(*self.ranges[term])
                 for term in shop_cost_types[name]
             }
         elif name in vanilla_location_costs:
@@ -512,7 +513,7 @@ def create_location(self, name: str, vanilla=False) -> HKLocation:
 
         region = self.multiworld.get_region("Menu", self.player)
 
-        if vanilla and not self.multiworld.AddUnshuffledLocations[self.player]:
+        if vanilla and not self.options.AddUnshuffledLocations:
             loc = HKLocation(self.player, name,
                              None, region, costs=costs, vanilla=vanilla,
                              basename=basename)
@@ -560,26 +561,26 @@ def remove(self, state, item: HKItem) -> bool:
         return change
 
     @classmethod
-    def stage_write_spoiler(cls, world: MultiWorld, spoiler_handle):
-        hk_players = world.get_game_players(cls.game)
+    def stage_write_spoiler(cls, multiworld: MultiWorld, spoiler_handle):
+        hk_players = multiworld.get_game_players(cls.game)
         spoiler_handle.write('\n\nCharm Notches:')
         for player in hk_players:
-            name = world.get_player_name(player)
+            name = multiworld.get_player_name(player)
             spoiler_handle.write(f'\n{name}\n')
-            hk_world: HKWorld = world.worlds[player]
+            hk_world: HKWorld = multiworld.worlds[player]
             for charm_number, cost in enumerate(hk_world.charm_costs):
                 spoiler_handle.write(f"\n{charm_names[charm_number]}: {cost}")
 
         spoiler_handle.write('\n\nShop Prices:')
         for player in hk_players:
-            name = world.get_player_name(player)
+            name = multiworld.get_player_name(player)
             spoiler_handle.write(f'\n{name}\n')
-            hk_world: HKWorld = world.worlds[player]
+            hk_world: HKWorld = multiworld.worlds[player]
 
-            if world.CostSanity[player].value:
+            if hk_world.options.CostSanity:
                 for loc in sorted(
                     (
-                        loc for loc in itertools.chain(*(region.locations for region in world.get_regions(player)))
+                        loc for loc in itertools.chain(*(region.locations for region in multiworld.get_regions(player)))
                         if loc.costs
                     ), key=operator.attrgetter('name')
                 ):
@@ -603,15 +604,15 @@ def get_filler_item_name(self) -> str:
                     'RandomizeGeoRocks', 'RandomizeSoulTotems', 'RandomizeLoreTablets', 'RandomizeJunkPitChests',
                     'RandomizeRancidEggs'
             ):
-                if getattr(self.multiworld, group):
+                if getattr(self.options, group):
                     fillers.extend(item for item in hollow_knight_randomize_options[group].items if item not in
                                    exclusions)
             self.cached_filler_items[self.player] = fillers
-        return self.multiworld.random.choice(self.cached_filler_items[self.player])
+        return self.random.choice(self.cached_filler_items[self.player])
 
 
-def create_region(world: MultiWorld, player: int, name: str, location_names=None) -> Region:
-    ret = Region(name, player, world)
+def create_region(multiworld: MultiWorld, player: int, name: str, location_names=None) -> Region:
+    ret = Region(name, player, multiworld)
     if location_names:
         for location in location_names:
             loc_id = HKWorld.location_name_to_id.get(location, None)
@@ -684,42 +685,7 @@ def _hk_notches(self, player: int, *notches: int) -> int:
         return sum(self.multiworld.worlds[player].charm_costs[notch] for notch in notches)
 
     def _hk_option(self, player: int, option_name: str) -> int:
-        return getattr(self.multiworld, option_name)[player].value
+        return getattr(self.multiworld.worlds[player].options, option_name).value
 
     def _hk_start(self, player, start_location: str) -> bool:
-        return self.multiworld.StartLocation[player] == start_location
-
-    def _hk_nail_combat(self, player: int) -> bool:
-        return self.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player)
-
-    def _hk_can_beat_thk(self, player: int) -> bool:
-        return (
-            self.has('Opened_Black_Egg_Temple', player)
-            and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1
-            and self._hk_nail_combat(player)
-            and (
-                self.has_any({'LEFTDASH', 'RIGHTDASH'}, player)
-                or self._hk_option(player, 'ProficientCombat')
-            )
-            and self.has('FOCUS', player)
-        )
-
-    def _hk_siblings_ending(self, player: int) -> bool:
-        return self._hk_can_beat_thk(player) and self.has('WHITEFRAGMENT', player, 3)
-
-    def _hk_can_beat_radiance(self, player: int) -> bool:
-        return (
-            self.has('Opened_Black_Egg_Temple', player)
-            and self._hk_nail_combat(player)
-            and self.has('WHITEFRAGMENT', player, 3)
-            and self.has('DREAMNAIL', player)
-            and (
-                (self.has('LEFTCLAW', player) and self.has('RIGHTCLAW', player))
-                or self.has('WINGS', player)
-            )
-            and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1
-            and (
-                (self.has('LEFTDASH', player, 2) and self.has('RIGHTDASH', player, 2))  # Both Shade Cloaks
-                or (self._hk_option(player, 'ProficientCombat') and self.has('QUAKE', player))  # or Dive
-            )
-        )
+        return self.multiworld.worlds[player].options.StartLocation == start_location