local Migrate = {}

-------------------------------------------------------
-- MOD SPECIFIC PARAMETERS
-- If you're copying this file to another mod,
-- make sure to modify the constants and methods below.
-------------------------------------------------------

local function added_to_existing_game()
  local tick_task = new_tick_task("game-message") --[[@as GameMessageTickTask]]
  tick_task.message = {"space-exploration.warn_added_to_existing_game"}
  tick_task.delay_until = game.tick + 180 --3s
end

-- ignore_techs don't cause their children to get locked.
-- Mainly used for newly added techs.
local ignore_techs = {
  mod_prefix.."rocket-science-pack",
  mod_prefix.."space-belt",
  mod_prefix.."space-pipe",
  mod_prefix.."pyroflux-smelting",
  mod_prefix.."condenser-turbine",
  "utility-science-pack",
  "production-science-pack",
  "space-science-pack",
  mod_prefix.."space-biochemical-laboratory",
}
Migrate.ignore_techs = {}
for _, tech in pairs(ignore_techs) do
  Migrate.ignore_techs[tech] = tech
end

-- chainbreak_techs are not locked by their prerequisites and don't propagate a tech locking chain
-- mainly used if a section of the tech tree has moved
local chainbreak_techs = {
  "utility-science-pack",
  "production-science-pack",
  mod_prefix.."space-solar-panel",
  mod_prefix.."space-data-card",
  mod_prefix.."space-radiator-1",
}
Migrate.chainbreak_techs = {}
for _, tech in pairs(chainbreak_techs) do
  Migrate.chainbreak_techs[tech] = tech
end

-- dont_lock_techs can't be locked by their prerequisites being locked.
local dont_lock_techs = {
  mod_prefix.."naquium-cube",
  mod_prefix.."naquium-tessaract",
  mod_prefix.."naquium-processor",
  mod_prefix.."space-accumulator-2",
  mod_prefix.."wide-beacon-2",
  mod_prefix.."antimatter-production",
  mod_prefix.."antimatter-reactor",
  mod_prefix.."space-solar-panel-3",
  mod_prefix.. "space-probe",
  mod_prefix.. "dimensional-anchor",
  mod_prefix.. "long-range-star-mapping",
  mod_prefix.. "factory-spaceship-1",
  mod_prefix.. "factory-spaceship-2",
  mod_prefix.. "factory-spaceship-3",
  mod_prefix.. "factory-spaceship-4",
  mod_prefix.. "factory-spaceship-5",
  mod_prefix.. "lifesupport-equipment-4",
  mod_prefix.. "bioscrubber",
  "energy-shield-mk6-equipment",
  mod_prefix.. "spaceship-victory",
  mod_prefix.. "antimatter-engine",
  mod_prefix.. "fluid-burner-generator",
}
Migrate.dont_lock_techs = {}
for _, tech in pairs(dont_lock_techs) do
  Migrate.dont_lock_techs[tech] = tech
end

function Migrate.always_do_migrations()
  if not storage.universe_scale then
    storage.universe_scale =  (#storage.universe.stars + #storage.universe.space_zones) ^ 0.5 * Universe.stellar_average_separation
    Universe.separate_stellar_position()
    for _, zone in pairs(storage.zone_index) do
      if zone.type == "planet" then
        Universe.planet_gravity_well_distribute(zone)
      end
    end
  end

  -- general cleaning
  for _, zone in pairs(storage.zone_index) do
    if zone.is_homeworld or zone.name == "Nauvis" then
      zone.tags = nil
    end
    if zone.tags then
      if zone.tags.moisture and zone.tags.moisture == "moisture_very_low" then
        -- was incorrect in universe.raw, if surface is genrated it is incorrect but don't change the terrain if already settled
        zone.tags.moisture = "moisture_low"
        Zone.delete_surface(zone) -- remove if unsettled
        log("Changed moisture tag from moisture_very_low to moisture_low.")
      end
    end
    Zone.set_solar_and_daytime(zone)
  end

  for _, player in pairs(game.players) do
    if player.character and player.permission_group and player.permission_group.name == RemoteView.name_permission_group then
      player.permission_group = nil
    end
  end

  for _, name in pairs({"se-remote-view", "se-remote-view_satellite"}) do
    local group = game.permissions.get_group(name)
    if group then group.destroy() end
  end

  for registration_number, resource_set in pairs(storage.core_seams_by_registration_number) do
    if not resource_set.resource.valid then
      CoreMiner.remove_seam(resource_set)
    end
  end

  Migrate.fill_tech_gaps(true)

  Ancient.update_unlocks()
end

---@param allow_whitelists boolean
function Migrate.fill_tech_gaps(allow_whitelists)
  local tech_children = {}
  for _, technology in pairs(prototypes.technology) do
    for _, prerequisite in pairs(technology.prerequisites) do
      tech_children[prerequisite.name] = tech_children[prerequisite.name] or {}
      table.insert(tech_children[prerequisite.name], technology.name)
    end
  end

  --first pass
  for _, force in pairs(game.forces) do
    if force.name ~= "enemy"
      and force.name ~= "neutral"
      and force.name ~= "capture"
      and force.name ~= "ignore"
      and force.name ~= "friendly"
      and force.name ~= "conquest" then

        if force.technologies[mod_prefix.."deep-space-science-pack-1"].researched then
          force.technologies[mod_prefix.."deep-catalogue-1"].researched = true
        end

        if force.technologies[mod_prefix.."space-assembling"].researched then
          force.technologies[mod_prefix.."space-belt"].researched = true
          force.technologies[mod_prefix.."space-pipe"].researched = true
        end

        if force.technologies[mod_prefix.."space-supercomputer-1"].researched then
          force.technologies[mod_prefix.."space-data-card"].researched = true
        end

        if force.technologies["uranium-processing"].researched then
          force.technologies[mod_prefix.."centrifuge"].researched = true
        end

        if force.technologies["nuclear-power"].researched then
          force.technologies["steam-turbine"].researched = true
        end

        local techs_done = {}
        local rocket_science = force.technologies[mod_prefix.."rocket-science-pack"]
        Migrate.fill_tech_gaps_rec(tech_children, techs_done, rocket_science, false, allow_whitelists)
    end
  end
end

---@param tech_children {[string]:string[]}
---@param techs_done Flags
---@param tech LuaTechnology
---@param lock boolean
---@param allow_whitelists boolean
function Migrate.fill_tech_gaps_rec(tech_children, techs_done, tech, lock, allow_whitelists)
  local change = false
  if allow_whitelists and Migrate.chainbreak_techs[tech.name] then
    -- break the lock chain
    lock = false
  end
  if not(tech.researched or tech.level > 1) then
    if (not allow_whitelists) or (not Migrate.dont_lock_techs[tech.name]) and (not Migrate.ignore_techs[tech.name]) then
      lock = true
    end
  end
  if lock then
    if tech.researched then
      change = true
    end
    if not (allow_whitelists and Migrate.dont_lock_techs[tech.name]) then
      if tech.researched then
        tech.researched = false
        Log.debug({"", "Unresearch ", "technology-name."..tech.name})
      end
    end
  end
  if tech_children[tech.name] and (change or not techs_done[tech.name]) then
    for _, child_name in pairs(tech_children[tech.name]) do
      Migrate.fill_tech_gaps_rec(tech_children, techs_done, tech.force.technologies[child_name], lock, allow_whitelists)
    end
  end
  techs_done[tech.name] = true
end

-- Find dummy recipes, reset the recipes, spill removed items
local function replace_dummy_recipes(entity_names)
  for _, surface in pairs(game.surfaces) do
    local machines = surface.find_entities_filtered({name=entity_names})
    for _, machine in pairs(machines) do
      local recipe = machine.get_recipe()
      if recipe and string.starts(recipe.name, Shared.dummy_migration_recipe_prefix) then
        local original_recipe_name = recipe.name:sub(string.len(Shared.dummy_migration_recipe_prefix)+1)
        local removed_items = machine.set_recipe(original_recipe_name)
        for item_name, count in pairs(removed_items) do
          surface.spill_item_stack{
            position = machine.position,
            stack = {name=item_name, count=count},
            enable_looted = true,
            force = machine.force,
            allow_belts = false
          }
        end
      end
    end
  end
end

---------------------------------------------------------------
-- Mod-specific parameters end here, migration code starts here
---------------------------------------------------------------


--Converts the default version string to one that can be lexographically compared using string comparisons
---@param ver string String in a version format of 'xxx.xxx.xxx' where each xxx is in the range 0-65535
---@return string version Version number with leading padded 0's for use in string comparisons
function Migrate.version_to_comparable_string(ver)
  return string.format("%05d.%05d.%05d", string.match(ver, "^(%d+)%.(%d+)%.(%d+)$"))
end

--Converts the lexographically coaprable version string to default, human readable format
---@param ver string String in a version format of 'xxxxx.xxxxx.xxxxx' where each xxxxx is a number with potential leading 0's
---@return string version Version number with leading 0's stripped
function Migrate.comparable_string_to_version(ver)
  return (string.gsub(ver, "^0*(%d-%d).0*(%d-%d).0*(%d-%d)$", "%1.%2.%3"))
end

---@param event ConfigurationChangedData
function Migrate.do_migrations(event)
  local mod_name = script.mod_name

  --check if we changed versions
  local mod_changes = event.mod_changes[mod_name]
  if mod_changes and mod_changes.old_version ~= mod_changes.new_version then
    if mod_changes.old_version == nil then
      added_to_existing_game()
      return
    end
    local old_ver_string = Migrate.version_to_comparable_string(mod_changes.old_version)
    local versions_to_migrate = {} --array of lexographically comparible migration version strings that need running
    local migrations = {} --migration version to migration function map
    --look for needed migrations
    for migrate_version, migration in pairs(Migrate.migrations) do
      local migrate_ver_string = Migrate.version_to_comparable_string(migrate_version)
      if migrate_ver_string > old_ver_string then
        table.insert(versions_to_migrate, migrate_ver_string)
        migrations[migrate_ver_string] = migration
      end
    end

    if next(versions_to_migrate) then
      log(mod_name.. ": Starting migrations from "..mod_changes.old_version.." to "..mod_changes.new_version.." " .. (script.level.is_simulation and "(simulation)" or ""))

      --ensure migrations run in the correct order
      table.sort(versions_to_migrate)

      --do migrations
      for _, version_to_migrate in pairs(versions_to_migrate) do
        log(mod_name..": Running migration "..Migrate.comparable_string_to_version(version_to_migrate))
        migrations[version_to_migrate](event)
      end
    end

    --do any test migrations
    for migration_name, migration in pairs(Migrate.test_migrations) do
      if not is_debug_mode then
        --there should never be any test migrations if not in debug mode
        local msg = "[color=red]WARNING:[/color] Test migration scripts exist while not in debug mode. These should be moved to live migrations with version labels prior to live release."
        log(msg)
        game.print(msg)
      end
      local msg = mod_name..": Running test migration: "..migration_name
      log(msg)
      game.print(msg)
      migration(event)
    end
  end
end

Migrate.migrations = {
  ["0.5.001"] = function()
    error("This save file was made on an old version of Space Exploration (before 0.5). Please update to 0.6 before updating to 0.7.")
  end,

  ["0.5.039"] = function()
    for _, force in pairs(game.forces) do
      if force.technologies[mod_prefix .. "space-supercomputer-1"].researched then
        force.print({"space-exploration.migration-recipe-changed", {"recipe-name."..mod_prefix .. "formatting-1"}})
      end
      if force.technologies[mod_prefix .. "space-supercomputer-2"].researched then
        force.print({"space-exploration.migration-recipe-changed", {"recipe-name."..mod_prefix .. "formatting-2"}})
      end
    end
  end,

  ["0.5.045"] = function()
    for _, force in pairs(game.forces) do
      if force.technologies[mod_prefix .. "spaceship"].researched then
        force.print({"space-exploration.migration-recipe-changed", {"recipe-name."..mod_prefix .. "spaceship-floor"}})
      end
    end
  end,

  ["0.5.050"] = function()
    storage.playerdata = storage.playerdata or {}
    for _, player in pairs(game.players) do
      RemoteView.make_history_valid(player)
    end
  end,

  ["0.5.053"] = function()
    -- destroy any open GUIs for zonelist in center since now its in screen
    -- and we won't listen for the close event properly in center
    for _, player in pairs(game.players) do
      if player and player.gui and player.gui.center then
        local zonelist_gui = player.gui.center["se-zonelist_main"]
        if zonelist_gui then
          zonelist_gui.destroy()
        end
      end
    end
    -- destroy any open GUIs for delivery cannon since we no longer
    -- listen to the close window button in that place
    for _, player in pairs(game.players) do
      if player.gui.left[DeliveryCannonGUI.name_delivery_cannon_gui_root] then
        player.gui.left[DeliveryCannonGUI.name_delivery_cannon_gui_root].destroy()
      end
    end
    -- destroy any open GUIS for landing pads since we no longer
    -- listen to the close window button in that place
    for _, player in pairs(game.players) do
      if player.gui.left[LandingpadGUI.name_rocket_landing_pad_gui_root] then
        player.gui.left[LandingpadGUI.name_rocket_landing_pad_gui_root].destroy()
      end
    end
  end,

  ["0.5.056"] = function()
    -- destroy any open GUIS for launchpads since we no longer
    -- listen to the close window button in that place
    for _, player in pairs(game.players) do
      if player.gui.left[LaunchpadGUI.name_rocket_launch_pad_gui_root] then
        player.gui.left[LaunchpadGUI.name_rocket_launch_pad_gui_root].destroy()
      end
    end
    -- destroy any open GUIS for landing pads since we no longer
    -- listen to the close window button in that place
    for _, player in pairs(game.players) do
      if player.gui.left[EnergyBeamGUI.name_transmitter_gui_root] then
        player.gui.left[EnergyBeamGUI.name_transmitter_gui_root].destroy()
      end
    end

    -- delete the map view surface so it can be regenerated prettier
    storage.playerdata = storage.playerdata or {}
    for _, player in pairs(game.players) do
      local surface_name = MapView.get_surface_name(player)
      if game.get_surface(surface_name) then
        MapView.stop_map(player)
        game.delete_surface(surface_name)
      end
    end
  end,

  ["0.5.060"] = function()
    for _, player in pairs(game.connected_players) do
      if player.gui and player.gui.screen and player.gui.screen[SpaceshipGUI.name_spaceship_gui_root] then
        player.gui.screen[SpaceshipGUI.name_spaceship_gui_root].destroy()
      end
    end

    -- make it so existing maps have the toggle for showing danger zones
    storage.playerdata = storage.playerdata or {}
    for _, player in pairs(game.players) do
      local settings = MapView.get_make_settings(player)
      if settings then
        settings.show_danger_zones = true
      end
    end

    -- close old spaceship UIs
    for _, player in pairs(game.players) do
      SpaceshipGUI.gui_close(player)
    end

    storage.spaceship_clamps = {}
    storage.spaceship_clamps_by_surface = {}
    -- migrate the clamps to have the internal power poles for passthrough
    for _, surface in pairs(game.surfaces) do
      local clamps = surface.find_entities_filtered{name=SpaceshipClamp.name_spaceship_clamp_keep}
      for _, clamp in pairs(clamps) do
        local position = table.deepcopy(clamp.position)
        local direction = table.deepcopy(clamp.direction)
        local force = clamp.force
        local clamp_comb = clamp.get_or_create_control_behavior() --[[@as LuaConstantCombinatorControlBehavior]]
        local clamp_signal = clamp_comb.get_section(1).get_slot(1)
        clamp.destroy{
          raise_destroy=true
        }
        local migrated_clamp = surface.create_entity{
          name=SpaceshipClamp.name_spaceship_clamp_keep,
          position=position,
          direction=direction,
          force=force,
          raise_built=true
        }
        if migrated_clamp then
          migrated_clamp.rotatable = false
          local migrated_comb = migrated_clamp.get_or_create_control_behavior() --[[@as LuaConstantCombinatorControlBehavior]]
          if next(clamp_signal) and migrated_comb and migrated_comb.valid then
            migrated_comb.get_section(1).set_slot(1, clamp_signal)
          end
          SpaceshipClamp.validate_clamp_signal(migrated_clamp)
        end
      end
    end

    -- clear engines becusae old efficiency calculation will be too high
    -- and engines are more powerful now.
    for _, spaceship in pairs(storage.spaceships) do
      spaceship.engines = nil
      Spaceship.start_integrity_check(spaceship)
    end
    storage.forces = storage.forces or {}
    for _, delivery_cannon in pairs(storage.delivery_cannons) do
      DeliveryCannon.add_delivery_cannon_to_table(delivery_cannon) -- assigns to zone_assets
    end

    for _, force in pairs(game.forces) do
      if force.technologies[mod_prefix .. "spaceship"].researched then
        force.print({"space-exploration.migrate_0_5_056"})
      end
    end
  end,

  ["0.5.064"] = function()
    -- The tooltip for the starmap button changed.
    storage.forces = storage.forces or {}
    for _, player in pairs(game.players) do
      MapView.update_overhead_button(player.index)
    end
  end,

  ["0.5.073"] = function()
    game.print({"space-exploration.migrate_0_5_073"})
    Universe.update_zones_minimum_threat(false)
  end,

  ["0.5.094"] = function()
    storage.zones_by_name = storage.zones_by_name or {}
    storage.zone_index = storage.zone_index or {}
    Zone.fix_out_of_map_tiles()
  end,

  ["0.5.095"] = function()
    -- This was previously being done as part of Universe Explorer gui update logic
    Universe.set_hierarchy_values()

    -- Run for _all_ players, including disconnected ones
    storage.playerdata = storage.playerdata or {}
    storage.spaceships = storage.spaceships or {}
    storage.zones_by_surface = storage.zones_by_surface or {}
    storage.zone_index = storage.zone_index or {}
    storage.zones_by_name = storage.zones_by_name or {}
    storage.forces = storage.forces or {}
    storage.ruins = storage.ruins or {}
    storage.glyph_vaults = storage.glyph_vaults or {}
    for _, player in pairs(game.players) do
      if player.gui.screen["se-zonelist_main"] then
        player.gui.screen["se-zonelist_main"].destroy()
        Zonelist.open(player)
      end
    end
  end,

  ["0.5.101"] = function()
    Krastorio2.disable_spaceship_victory_tech_on_migrate()
  end,

  ---The "charger" entities are being changed to electric-turrets from EEIs. The game will swap the
  ---old entities out for the new ones, though Lua references to the previous EEI chargers will
  ---become invalid. `defence.charger` needs to be updated to point to the new charger LuaEntity.
  ---This function will also populate the newly created `meteor_defences` and
  ---`meteor_point_defences` saved to `global` and clean up some old or invalid launchpad entities.
  ["0.5.104"] = function()
    -- Clean up launch old or invalid launchpad entities
    ---@class RocketLaunchPadInfo
    ---@field package settings any DEPRECIATED
    ---@class RocketLandingPadInfo
    ---@field package settings any DEPRECIATED
    ---@class MeteorDefenceInfo
    ---@field package extra_overlay_id any DEPRECIATED
    for _, launch_pad in pairs(storage.rocket_launch_pads) do
      if launch_pad.rocket_entity and not launch_pad.rocket_entity.valid then
        launch_pad.rocket_entity = nil
      end
      if launch_pad.settings then launch_pad.settings = nil end
    end

    for _, landing_pad in pairs(storage.rocket_landing_pads) do
      if landing_pad.settings then landing_pad.settings = nil end
    end

    -- Ensure a storage table for meteor zones exists
    if not storage.meteor_zones then
      local nauvis = Zone.from_name("Nauvis") --[[@as PlanetType]]
      storage.meteor_zones = {[nauvis.index]=nauvis}
    end

    -- Create a meteor schedule table and populate it
    storage.meteor_schedule = {}
    for _, zone in pairs(storage.meteor_zones) do
      local target = (zone.type == "orbit" and Zone.is_solid(zone.parent)) and zone.parent or zone
      Meteor.schedule_meteor_shower{zone=target, tick=(zone.next_meteor_shower or game.tick+3600)}
    end

    storage.meteor_defences = {}
    storage.meteor_point_defences = {}

    local gl_charger_name = Meteor.name_meteor_defence_charger
    local pt_charger_name = Meteor.name_meteor_point_defence_charger
    local pt_charger_overcharged_name = Meteor.name_meteor_point_defence_charger_overcharged
    local charger_unit_numbers = {}

    for _, zone in pairs(storage.zone_index) do
      -- Collect meteor defenses
      for index, defence in pairs(zone.meteor_defences or {}) do
        if defence.container and defence.container.valid then
          -- Try to find the colocated charger
          defence.charger = defence.container.surface.find_entity(gl_charger_name, defence.container.position)

          if defence.charger then
            defence.zone = zone
            defence.type = "global"
            charger_unit_numbers[defence.charger.unit_number] = true
            table.insert(storage.meteor_defences, defence)
          else
            -- Charger not found; destroy container, and remove table reference from zone
            defence.container.destroy()
            zone.meteor_defences[index] = nil
          end
        else
          -- If container is invalid, remove this table reference from zone.
          zone.meteor_defences[index] = nil
        end
      end

      -- Collect meteor point defenses
      for index, defence in pairs(zone.meteor_point_defences or {}) do
        if defence.container and defence.container.valid then
          local position = defence.container.position
          -- Try to find the colocated charger
          defence.charger =
            defence.container.surface.find_entity(pt_charger_name, position) or
            defence.container.surface.find_entity(pt_charger_overcharged_name, position)

          if defence.charger then
            -- Do this for the charger to render on top
            defence.container.teleport(defence.container.position)
            defence.zone = zone
            defence.type = "point"
            defence.mode = (defence.charger.name == pt_charger_overcharged_name) and "fast" or "normal"
            charger_unit_numbers[defence.charger.unit_number] = true
            table.insert(storage.meteor_point_defences, defence)
          else
            -- Charger not found; destroy container and remove table reference from zone
            defence.container.destroy()
            zone.meteor_point_defences[index] = nil
          end
        else
          -- If container is invalid, remove this table reference from zone.
          zone.meteor_point_defences[index] = nil
        end

        -- Delete the extra overlay tint used for overcharge since it's not working
        if defence.extra_overlay_id and defence.extra_overlay_id.valid then
          defence.extra_overlay_id.destroy()
        end
        defence.extra_overlay_id = nil
      end
    end

    -- Destroy any orphaned or duplicate charger entities
    for _, zone in pairs(storage.meteor_zones) do
      local surface = Zone.get_surface(zone)
      if surface then
        local chargers = surface.find_entities_filtered{
          name={gl_charger_name, pt_charger_name, pt_charger_overcharged_name}}

        for _, charger in pairs(chargers) do
          if not charger_unit_numbers[charger.unit_number] then
            charger.destroy()
          end
        end
      end
    end

    -- Destroy any MPD range circles leftover from 0.1.x
    for _, playerdata in pairs(storage.playerdata) do
      ---@class PlayerData
      ---@field package meteor_point_defence_radius any DEPRECATED
      if playerdata.meteor_point_defence_radius and playerdata.meteor_point_defence_radius.valid then
        playerdata.meteor_point_defence_radius.destroy()
        playerdata.meteor_point_defence_radius = nil
      end
    end

    -- Hide clouds on space surfaces (excluding spaceships)
    for _, surface in pairs(game.surfaces) do
      local zone = Zone.from_surface(surface)
      if zone and Zone.is_space(zone) and zone.type ~= "spaceship" then
        ---@cast zone -PlanetType, -MoonType, -SpaceshipType
        surface.show_clouds = false
      end
    end
  end,

  -- Revalidate meteor defense entities.
  ["0.5.106"] = function()
    -- Validate meteor defences
    for _, zone in pairs(storage.zone_index) do
      for unit_number, def in pairs(zone.meteor_defences or {}) do
        if not def.container.valid or not def.charger.valid then
          if def.container.valid then def.container.destroy() end
          if def.charger.valid then def.charger.destroy() end

          -- Delete defence table from global array
          local idx = Meteor.get_any_defence_index(unit_number, "global")
          if idx then table.remove(storage.meteor_defences, idx) end

          -- Delete defense table from zone's meteor defences table
          zone.meteor_defences[unit_number] = nil
        end
      end

      -- Validate meteor point defences
      for unit_number, def in pairs(zone.meteor_point_defences or {}) do
        if not def.container.valid or not def.charger.valid then
          if def.container.valid then def.container.destroy() end
          if def.charger.valid then def.charger.destroy() end

          -- Delete defence table from global array
          local idx = Meteor.get_any_defence_index(unit_number, "point")
          if idx then table.remove(storage.meteor_point_defences, idx) end

          -- Delete defense table from zone's meteor defences table
          zone.meteor_point_defences[unit_number] = nil
        end
      end
    end
  end,

  ["0.5.113"] = function()
    -- Populate new bot_attrition field + Update space zones daytime from sunset to sunrise
    for _, zone in pairs(storage.zone_index) do
      Zone.calculate_base_bot_attrition(zone)
      Zone.set_solar_and_daytime(zone)
    end
    for _, spaceship in pairs(storage.spaceships) do
      Zone.calculate_base_bot_attrition(spaceship)
      Zone.set_solar_and_daytime(spaceship)
    end
  end,

  ["0.5.114"] = function()
    -- Some ground zones may have been daytime frozen by the previous migration
    for _, zone in pairs(storage.zone_index) do
      if Zone.is_solid(zone) then
        ---@cast zone PlanetType|MoonType
        Zone.set_solar_and_daytime(zone)
      end
    end
  end,

  ["0.5.115"] = function()
    -- Add hostiles_extinct flag
    for _, zone in pairs(storage.zone_index) do
      if zone.controls["enemy-base"] and
        zone.controls["enemy-base"].frequency == 0 and
        zone.controls["enemy-base"].size == -1 and
        zone.controls["enemy-base"].richness == -1 and
        not zone.plague_used then
        zone.hostiles_extinct = true
      end
    end
  end,

  ["0.6.001"] = function()
    ---@class storage
    ---@field package space_capsule_launches any DEPRECIATED

    -- Find all in-progress space capsule launches and finish them immediately
    storage.playerdata = storage.playerdata or {}
    storage.zone_index = storage.zone_index or {}
    storage.forces = storage.forces or {}
    storage.vgo = storage.vgo or {}
    storage.glyph_vaults = storage.glyph_vaults or {}
    storage.ruins = storage.ruins or {}
    storage.core_seams_by_registration_number = storage.core_seams_by_registration_number or {}
    storage.zones_by_surface = storage.zones_by_surface or {}
    storage.space_capsules = storage.space_capsules or {}
    for _, launch in pairs(storage.space_capsule_launches or {}) do
      -- Get passengers out of vehicle before destroying it
      if launch.vehicle and launch.vehicle.valid then
        launch.vehicle.set_driver(nil)
        launch.vehicle.set_passenger(nil)
        launch.vehicle.destroy()
      end

      local target_surface = Zone.get_make_surface(launch.destination_zone)
      local safe_pos = target_surface.find_non_colliding_position(
        Capsule.name_space_capsule_vehicle, launch.destination_position, 32, 1)
        or launch.destination_position

      target_surface.create_entity{
        name = Capsule.name_space_capsule_container,
        force = game.forces[launch.force_name],
        position = safe_pos,
        raise_built = true
      }

      for _, passenger in pairs(launch.passengers or {}) do
        if passenger.valid then
          passenger.destructible = true
          if remote.interfaces["jetpack"] and remote.interfaces["jetpack"]["unblock_jetpack"] then
            remote.call("jetpack", "unblock_jetpack", {character=passenger})
          end
          teleport_character_to_surface(passenger, target_surface, safe_pos)
        end
      end

      -- Clean up any leftover entities
      if launch.light and launch.light.valid then launch.light.destroy() end
      if launch.shadow and launch.shadow.valid then launch.shadow.destroy() end
      if launch.rocket_sound and launch.rocket_sound.valid then launch.rocket_sound.destroy() end
    end

    storage.space_capsule_launches = nil

    -- Find all landed space capsules and make them part of the new system.
    for _, surface in pairs(game.surfaces) do
      local capsule_vehicles = surface.find_entities_filtered{name=Capsule.name_space_capsule_vehicle}
      for _, capsule in pairs(capsule_vehicles) do
        local force = capsule.force
        local position = capsule.position

        capsule.destroy()

        -- Destroy old lights and shadows
        local light = surface.find_entities_filtered{
          area=util.position_to_area(position, 1),
          name=Capsule.name_space_capsule_vehicle_light}[1]
        local shadow = surface.find_entities_filtered{
          area=util.position_to_area(position, 1),
          name=Capsule.name_space_capsule_vehicle_shadow}[1]
        if light then light.destroy() end
        if shadow then shadow.destroy() end

        surface.create_entity{
          name = Capsule.name_space_capsule_container,
          force = force,
          position = position,
          raise_built = true
        }
      end
    end

    -- Destroy space capsule GUI if any player has it open
    for _, player in pairs(game.players) do
      if player.gui.left["se-space-capsule-gui"] then
        player.gui.left["se-space-capsule-gui"].destroy()
      end
    end

    -- Find all in-progress cargo rocket launches and teleport the player to final position
    for _, tick_task in pairs(storage.tick_tasks or {}) do
      if tick_task.type == "launchpad-journey" then
        ---@cast tick_task CargoRocketTickTask
        local destination_zone = tick_task.launching_to_destination.zone
        local destination_surface = Zone.get_make_surface(destination_zone)
        local position = tick_task.launching_to_destination.position

        for _, passenger in pairs(tick_task.passengers or {}) do
          if passenger.valid then
            passenger.destructible = true
            if remote.interfaces["jetpack"] and remote.interfaces["jetpack"]["unblock_jetpack"] then
              remote.call("jetpack", "unblock_jetpack", {character=passenger})
            end
            teleport_character_to_surface(passenger, destination_surface, position)
          end
        end

        ---@class CargoRocketTickTask
        ---@field package passengers any DEPRECATED
        tick_task.passengers = {}
        tick_task.seated_passengers = {}
      end
    end

    CoreMiner.create_core_mining_tables()
    for _, zone in pairs(storage.zone_index) do
      if Zone.is_solid(zone) and Zone.get_surface(zone) then
        ---@cast zone PlanetType|MoonType
        CoreMiner.generate_core_seam_positions(zone)
      end
    end

    Tech.ignore_research_finished = true
    -- Give alternate recipe techs for free if they already had it before the update
    for _, force in pairs(game.forces) do
      if force.technologies then
        if force.technologies[mod_prefix .. "astronomic-science-pack-1"].researched then
          force.technologies[mod_prefix .. "cargo-rocket-section-beryllium"].researched = true
        end

        if force.technologies[mod_prefix .. "processing-cryonite"].researched then
          force.technologies[mod_prefix .. "cryonite-lubricant"].researched = true
          force.technologies[mod_prefix .. "processing-methane-ice"].researched = true
          force.technologies[mod_prefix .. "processing-water-ice"].researched = true
        end

        if force.technologies[mod_prefix .. "processing-vulcanite"].researched then
          force.technologies[mod_prefix .. "vulcanite-rocket-fuel"].researched = true
          force.technologies[mod_prefix .. "pyroflux-smelting"].researched = true
          if force.technologies[mod_prefix .. "processing-holmium"].researched then
            force.technologies[mod_prefix .. "pyroflux-smelting-holmium"].researched = true
          end
          if force.technologies[mod_prefix .. "processing-beryllium"].researched then
            force.technologies[mod_prefix .. "pyroflux-smelting-beryllium"].researched = true
          end
        end

        if force.technologies[mod_prefix .. "processing-iridium"].researched then
          force.technologies[mod_prefix .. "heat-shielding-iridium"].researched = true
        end

        if force.technologies[mod_prefix .. "aeroframe-scaffold"].researched then
          force.technologies[mod_prefix .. "low-density-structure-beryllium"].researched = true
        end

        if force.technologies[mod_prefix .. "holmium-cable"].researched then
          force.technologies[mod_prefix .. "processing-unit-holmium"].researched = true
        end
      end
    end

    Tech.restore_tech_levels(mod_prefix.."rocket-cargo-safety")
    Tech.restore_tech_levels(mod_prefix.."rocket-reusability")
    Tech.restore_tech_levels(mod_prefix.."rocket-survivability")
    Tech.restore_tech_levels("artillery-shell-range")
    Tech.restore_tech_levels("artillery-shell-speed")
    Tech.ignore_research_finished = nil
    for _, force in pairs(game.forces) do
      if is_player_force(force.name) then
        Tech.record_force_technologies(force)
      end
    end

    game.print({"space-exploration.migrate_0_6_001"})
  end,

  ["0.6.004"] = function()
    storage.core_seams_by_registration_number = storage.core_seams_by_registration_number or {}
    storage.zones_by_name = storage.zones_by_name or {}
    storage.zones_by_surface = storage.zones_by_surface or {}
    CoreMiner.create_core_mining_tables()
    for _, zone in pairs(storage.zone_index) do
      if zone.special_type == "homeworld" then
        UniverseLegacy.make_validate_homesystem(zone)
      end
    end

  end,

  ["0.6.034"] = function()
    -- Re-disable the K2 transceiver win due to a bug not disabling it on inflight games previously.
    Krastorio2.disable_transceiver_win()

    -- Remove Advanced Beacon entities and place modules and Compact Beacon 1 in a box at the same position.
    for _, surface in pairs(game.surfaces) do
      local adv_beacons = surface.find_entities_filtered{name="kr-singularity-beacon"}
      for _, adv_beacon in pairs(adv_beacons) do
        local position = table.deepcopy(adv_beacon.position)
        local force = adv_beacon.force
        local modules = adv_beacon.get_module_inventory().get_contents()
        adv_beacon.destroy{
          raise_destroy=true
        }
        local migration_container = surface.create_entity{
          name="wooden-chest",
          position=position,
          force=force,
          raise_built=true
        }
        if migration_container then
          local inventory = migration_container.get_inventory(defines.inventory.chest)
          if inventory then
            for _, beacon_inv_item in pairs(modules) do
              inventory.insert({name=beacon_inv_item.name, count=beacon_inv_item.count, quality=beacon_inv_item.quality})
            end
            inventory.insert({name="se-compact-beacon", count=1})
          else
            log("Migration failed for surface " .. surface.name .. " location x: " .. position.x .. " y: " .. position.y)
          end
        end
      end
    end
  end,

  ["0.6.047"] = function()
    for _, force in pairs(game.forces) do
      if force.technologies["se-astronomic-science-pack-1"].researched then
        force.technologies["space-science-pack"].researched = true
      end
    end
  end,

  ["0.6.052"] = function()

    if storage.resources_and_controls then
      if storage.resources_and_controls.core_fragments then
        table.sort(storage.resources_and_controls.core_fragments) -- ignore the order change
      end
      -- allow water in orbit without changing all reosurces.
      if storage.resources_and_controls.resource_settings and storage.resources_and_controls.resource_settings[mod_prefix.."water-ice"] then
        storage.resources_and_controls.resource_settings[mod_prefix.."water-ice"].allowed_for_zone.orbit = true
      end
      storage.resources_and_controls_compare_string = util.table_to_string(storage.resources_and_controls)
    end

    local orbits_cleared = {}

    storage.forces = storage.forces or {}
    storage.chart_tag_buffer = storage.chart_tag_buffer or {}
    storage.chart_tag_next_id = storage.chart_tag_next_id or 0
    storage.tick_tasks = storage.tick_tasks or {}
    storage.next_tick_task_id = storage.next_tick_task_id or 1
    for _, force in pairs(game.forces) do
      local force_name = force.name
      if SystemForces.is_system_force(force.name) then goto continue end
      local force_data = storage.forces[force_name]
      if not force_data then goto continue end
      local home_zone = Zone.get_force_home_zone(force_name)
      if not home_zone then home_zone = Zone.get_default() end
      local orbit_zone = home_zone.orbit
      if not orbit_zone then goto continue end
      local home_surface = home_zone.surface_index and game.get_surface(home_zone.surface_index)
      local orbit_surface = orbit_zone.surface_index and game.get_surface(orbit_zone.surface_index)

      local satellite_position = force_data.nauvis_satellite
      force_data.nauvis_satellite = nil
      local weapons_cache = false
      if force_data.cargo_rockets_launched == 0 then
        local satellites_launched = force_data.satellites_launched
        if satellites_launched > 0 then
          -- Delete old satellite
          if orbit_surface then
            local v0_6_satellite_exists = orbit_surface.count_entities_filtered{name = "se-space-legacy-straight-rail"} > 0
            if v0_6_satellite_exists then
              orbit_zone.ruins = orbit_zone.ruins or {}
              orbit_zone.ruins["satellite2"] = satellite_position
            else
              if not orbits_cleared[orbit_surface.index] then
                orbits_cleared[orbit_surface.index] = true
                -- multiple forces may have the same orbit
                if orbit_surface.count_entities_filtered{name = Landingpad.name_rocket_landing_pad} == 0 then
                  game.print("Resetting " .. orbit_zone.name)
                  local orbit_tiles = orbit_surface.find_tiles_filtered{
                    name = {"se-space-platform-plating", "se-space-platform-scaffold"}
                  }
                  local replacement_tiles = {}
                  for _, tile in pairs(orbit_tiles) do
                    table.insert(replacement_tiles, {position = tile.position, name = "se-space"})
                  end
                  orbit_surface.set_tiles(replacement_tiles)

                  -- Clean up entities (like pylons) that weren't deleted by tile changes
                  local orbit_entities = orbit_surface.find_entities_filtered{
                    name = "se-pylon-construction-radar",
                  }
                  for _, entity in pairs(orbit_entities) do
                    entity.destroy{raise_destroy = true}
                  end

                  -- Delete old map tag
                  for _, tag in pairs(force.find_chart_tags(orbit_surface)) do
                    if tag.text == "Space Platform" then
                      tag.destroy()
                    end
                  end
                end
              end
            end
          end
          if satellites_launched >= 1 then
            weapons_cache = true
          end
          if satellites_launched >= 2 then
            build_satellite(force_name)
          end
        end
      else
        weapons_cache = true
      end
      if weapons_cache then
        local tick_task = new_tick_task("weapons-cache") --[[@as WeaponsCacheTickTask]]
        tick_task.force_name = force_name
        tick_task.delay_until = game.tick + 15 * 60
        tick_task.surface = home_surface
      end
      ::continue::
    end
  end,

  ["0.6.061"] = function()
    storage.core_seams_by_registration_number = storage.core_seams_by_registration_number or {}
    CoreMiner.reset_seams(Zone.from_name("Nauvis") --[[@as PlanetType]])
  end,

  ["0.6.066"] = function()
    storage.core_seams_by_registration_number = storage.core_seams_by_registration_number or {}
    for surface_index, zone in pairs(storage.zones_by_surface) do -- skip surfaceless zones
      if zone.core_seam_resources then
        for resource_index, resource_set in pairs(zone.core_seam_resources) do
          if not resource_set.resource_index then
            resource_set.resource_index = resource_index
            resource_set.zone_index = zone.index
            if not resource_set.resource.valid then
              CoreMiner.remove_seam(resource_set)
            else
              CoreMiner.register_seam_resource(resource_set)
            end
          end
        end
      end
    end
  end,

  ["0.6.071"] = function()
    for _, entity in pairs(game.get_surface(1).find_entities_filtered{name="se-biter-friend"}) do
      if entity.force.name == "enemy" then
        entity.force = "player"
      end
    end

    storage.core_seams_by_registration_number = storage.core_seams_by_registration_number or {}
    for _, zone in pairs(storage.zones_by_surface) do
      if zone.core_seam_resources then
        CoreMiner.validate_core_seam_resources_table(zone)
      end
    end
  end,

  ["0.6.074"] = function()
    -- Clean up any orphaned entries in global core seam registry
    for registration_number, resource_set in pairs(storage.core_seams_by_registration_number or {}) do
      local resource = resource_set.resource
      local fissure = resource_set.fissure

      if not (resource and resource.valid) and not (fissure and fissure.valid) then
        if resource_set.smoke_generator and resource_set.smoke_generator.valid then
          resource_set.smoke_generator.destroy()
        end

        storage.core_seams_by_registration_number[registration_number] = nil
      end
    end

    -- Ensure all core seam resources and fissures are registered
    storage.core_seams_by_registration_number = storage.core_seams_by_registration_number or {}
    for _, zone in pairs(storage.zones_by_surface) do
      for _, resource_set in pairs(zone.core_seam_resources or {}) do
        CoreMiner.register_seam_resource(resource_set)
        CoreMiner.register_seam_fissure(resource_set)
      end
    end
  end,

  ["0.6.079"] = function()
    -- Remove forcedata from mod lab forces
    for force_name, _ in pairs(storage.forces) do
      if SystemForces.is_system_force(force_name) then
        storage.forces[force_name] = nil
      end
    end
    ---@class storage
    ---@field package biter_friends_by_registration_number any DEPRECIATED
    -- Update AI settings for existing biter friends then delete biter_friends_by_registration_number table
    for _, biter_friend in pairs(storage.biter_friends_by_registration_number or {}) do
      if biter_friend.entity and biter_friend.entity.valid then
        biter_friend.entity.ai_settings.allow_destroy_when_commands_fail = false
        biter_friend.entity.ai_settings.allow_try_return_to_spawner = false
      end
    end
    storage.biter_friends_by_registration_number = nil
  end,

  ["0.6.082"] = function()
    -- Fix spaceships without bot attrition value after 0.6.81 bug
    for _, spaceship in pairs(storage.spaceships) do
      if not spaceship.base_bot_attrition then
        Zone.calculate_base_bot_attrition(spaceship)
      end
    end
  end,

  ["0.6.086"] = function()
    -- Make boosters and landing pads movable with picker dollies
    if remote.interfaces["PickerDollies"] and remote.interfaces["PickerDollies"]["remove_blacklist_name"] then
      remote.call("PickerDollies", "remove_blacklist_name", "se-spaceship-rocket-booster-tank")
      remote.call("PickerDollies", "remove_blacklist_name", "se-spaceship-ion-booster-tank")
      remote.call("PickerDollies", "remove_blacklist_name", "se-spaceship-antimatter-booster-tank")
      remote.call("PickerDollies", "remove_blacklist_name", "se-rocket-landing-pad")
    end

    -- Close old Lifesupport GUIs
    for _, player in pairs(game.players) do
      if player.gui.left["se-lifesupport"] then
        player.gui.left["se-lifesupport"].destroy()
      end
    end
  end,

  ["0.6.087"] = function()
    -- Setup existing big turbines with the correct generator
    for _, surface in pairs(game.surfaces) do
      for _, furnace in pairs(surface.find_entities_filtered({name = "se-big-turbine"})) do
        -- JSON migration will have already replaced all generators with the NW version
        if furnace.direction == defines.direction.south or furnace.direction == defines.direction.east then
          local old_generator = CondenserTurbine.find_generator(furnace)
          if old_generator then -- Should always happen but just in case
            -- Delete NW entity
            local fluid_count = old_generator.remove_fluid({name = "se-decompressing-steam", amount = 10000})
            old_generator.destroy()

            -- Create new entity
            local new_generator = surface.create_entity({name = "se-big-turbine-generator-SE", position = furnace.position, force = furnace.force, direction = furnace.direction, surface = furnace.surface})
            ---@cast new_generator -?
            if fluid_count ~= 0 then new_generator.insert_fluid({name = "se-decompressing-steam", amount = math.abs(fluid_count), temperature = 5000}) end
          end
        end
      end
    end
  end,

  ["0.6.088"] = function()
    -- Make cache of players in remote view
    ---@class RocketLaunchPadInfo
    ---@field package section_input any DEPRECIATED

    storage.connected_players_in_remote_view = {}
    for player_index, playerdata in pairs(storage.playerdata) do
      local player = game.get_player(player_index)
      if playerdata.remote_view_active and player then
        if playerdata.satellite_light then
          storage.connected_players_in_remote_view[player_index] = {
            player = player,
            satellite_light = playerdata.satellite_light -- if nil, will be recreated by on_tick
          }
          ---@class PlayerData
          ---@field package satellite_light any DEPRECATED
          playerdata.satellite_light = nil
        else
          RemoteView.create_light(player_index)
        end
      end
    end

    -- Since the prototype was removed, we just need to clear any remaining references
    for _, struct in pairs(storage.rocket_launch_pads) do
      struct.section_input = nil
    end

    -- Close old Nav-sat GUI and open new one if appropriate
    for _, player in pairs(game.players) do
      if player.gui.left["se-remote-view"] then
        player.gui.left["se-remote-view"].destroy()
        RemoteViewGUI.open(player)
      end
    end

    -- Close old Universe Explorer if any players have it open
    for _, player in pairs(game.players) do
      if player.gui.screen["se-zonelist_main"] then
        player.gui.screen["se-zonelist_main"].destroy()
      end
    end

    -- Reset/rename some `playerdata` fields related to the UE
    for _, playerdata in pairs(storage.playerdata) do
      playerdata.sort_criteria = nil
      playerdata.zonelist_filter_excludes = nil
      ---@class PlayerData
      ---@field package zonelist_zone_rows any DEPRECATED
      playerdata.zonelist_zone_rows = nil
    end
  end,

  ["0.6.095"] = function()
    -- Change tick_task data related to any chain beam that may be currently being fired
    for _, tick_task in pairs(storage.tick_tasks) do
      if tick_task.type == "chain-beam" then
        ---@cast tick_task ChainBeamTickTask
        tick_task.inverted_forces = {}
        for _, force in pairs(game.forces) do
          if force ~= tick_task.instigator_force then
            table.insert(tick_task.inverted_forces, force)
          end
        end
        tick_task.desired_vector = util.vector_multiply(tick_task.initial_vector, 2)
        ---@class ChainBeamTickTask
        ---@field package initial_vector any DEPRECATED
        tick_task.initial_vector = nil
        tick_task.bonus_damage = Shared.tesla_base_damage * tick_task.instigator_force.get_ammo_damage_modifier(Shared.tesla_ammo_category)
      end
    end
  end,

  ["0.6.100"] = function()
    -- Clean up orphaned space capsule shadows
    storage.zones_by_surface = storage.zones_by_surface or {}
    storage.spaceships = storage.spaceships or {}
    for _, surface in pairs(game.surfaces) do
      local zone = Zone.from_surface(surface)
      if zone then
        -- Shadows
        local shadows = surface.find_entities_filtered{name=Capsule.name_space_capsule_vehicle_shadow}
        for _, shadow in pairs(shadows) do
          local shadow_position = shadow.position

          -- Search for a superimposed capsule (scorched or otherwise)
          local match = (surface.find_entity(Capsule.name_space_capsule_vehicle, shadow_position) or
            surface.find_entity(Capsule.name_space_capsule_scorched_vehicle, shadow_position)) and true

          -- Handle launching capsules
          if not match then
            for _, space_capsule in pairs(storage.space_capsules or {}) do
              if space_capsule.shadow == shadow then
                match = true
                break
              end
            end
          end

          -- Destroy shadow as it is presumably orphaned
          if not match then shadow.destroy() end
        end

        -- Lights
        local lights = surface.find_entities_filtered{name=Capsule.name_space_capsule_vehicle_light}
        for _, light in pairs(lights) do
          local light_position = light.position

          -- Search for a superimposed capsule
          local match = surface.find_entity(Capsule.name_space_capsule_vehicle, light_position) and true

          -- Handle launching capsules
          if not match then
            for _, space_capsule in pairs(storage.space_capsules or {}) do
              if space_capsule.light == light then
                match = true
                break
              end
            end
          end

          -- Destroy light as it is presumably orphaned
          if not match then light.destroy() end
        end
      end
    end
  end,

  ["0.6.102"] = function()
    -- change all smoke generators from default force of "enemy" to "neutral"
    for _, surface in pairs(game.surfaces) do
      local smoke_gens = surface.find_entities_filtered{name="se-core-seam-smoke-generator"}
      for _, entity in pairs(smoke_gens) do
        entity.force = "neutral"
      end
    end
  end,

  ["0.6.105"] = function()
    -- Replace bot_attrition attribute with base_bot_attrition, and recalculate.
    ---@class SpaceshipType
    ---@field package bot_attrition? number DEPRECIATED
    ---@class PlanetType
    ---@field package bot_attrition? number DEPRECIATED
    ---@class StarType
    ---@field package bot_attrition? number DEPRECIATED
    for _, zone in pairs(storage.zone_index) do
      Zone.calculate_base_bot_attrition(zone)
      if zone.bot_attrition and math.abs(Zone.get_bot_attrition(zone) - zone.bot_attrition) > 0.001 then
        log("Attrition diff on zone " .. zone.name .. " ("..zone.type..") - " .. zone.bot_attrition .. " to " .. Zone.get_bot_attrition(zone))
      end
      zone.bot_attrition = nil
    end
    for _, spaceship in pairs(storage.spaceships) do
      Zone.calculate_base_bot_attrition(spaceship)
      if spaceship.bot_attrition and math.abs(Zone.get_bot_attrition(spaceship) - spaceship.bot_attrition) > 0.001 then
        log("Attrition diff on zone " .. spaceship.name .. " - " .. spaceship.bot_attrition .. " to " .. Zone.get_bot_attrition(spaceship))
      end
      spaceship.bot_attrition = nil
    end
  end,

  ["0.6.106"] = function()
    -- Some zones could have missing base bot attrition values after doing Zone.set_zone_as_homeworld
    -- Recalculate bot attrition for missing values
    for _, zone in pairs(storage.zone_index) do
      if not zone.base_bot_attrition then
        Zone.calculate_base_bot_attrition(zone)
      end
    end
  end,

  ["0.6.107"] = function()
    -- Universes created in 0.6.105 and 0.6.106 could have messed up enemy-base tables due to not deep copying
    -- Fully reset all zone controls to make sure they don't share any table reference
    for _, zone in pairs(storage.zone_index) do
      if Zone.is_solid(zone) then
        ---@cast zone PlanetType|MoonType
        Universe.apply_control_tags(zone.controls, zone.tags) -- Reset controls to tag values
        Universe.update_zone_minimum_threat(zone) -- Apply vitamelange minimum
        Zone.apply_flags_to_controls(zone) -- Apply plague and extinction
      end
    end
  end,

  ["0.6.108"] = function()
    -- Zone.apply_flags_to_controls did not correctly apply controls to existing surfaces mapgen for hostiles_extinct flag.
    -- Do it again.
    for _, zone in pairs(storage.zone_index) do
      if Zone.is_solid(zone) and zone.hostiles_extinct then
        ---@cast zone PlanetType|MoonType
        local surface = Zone.get_surface(zone)
        if surface then

          local mapgen = surface.map_gen_settings
          local mapgen_enemy_base = mapgen.autoplace_controls["enemy-base"]
          if mapgen_enemy_base.frequency ~= 0 or mapgen_enemy_base.size ~= -1 or mapgen_enemy_base.richness ~= -1 then
            -- Surface was affected by the bug, delete all biters
            local enemies = surface.find_entities_filtered({force = "enemy"})
            for _, entity in pairs(enemies) do
              entity.destroy()
            end
          end

          -- Fix mapgen
          Zone.apply_flags_to_controls(zone)
        end
      end
    end
  end,

  ["0.6.109"] = function()
    -- stop tracking non-SE linked containers
    if storage.linked_containers then
      for unit_number, linked_container in pairs(storage.linked_containers) do
        if not linked_container.container or not linked_container.container.valid or linked_container.container.name ~= "se-linked-container" then
          storage.linked_containers[unit_number] = nil
        end
      end
    end

    -- update elevator EEI capacity values and dis/connect wires if they must be
    storage.tick_tasks = storage.tick_tasks or {}
    storage.next_tick_task_id = storage.next_tick_task_id or 1
    if storage.space_elevators then
      for _, struct in pairs(storage.space_elevators) do
        if struct.is_primary then
          if struct.lua_energy and struct.energy_interface and struct.opposite_struct.energy_interface then -- redistribute power from lua_energy into the EEI's
            if struct.energy_interface.valid and struct.opposite_struct.energy_interface.valid then
              struct.energy_interface.electric_buffer_size = SpaceElevator.interface_energy_buffer
              struct.opposite_struct.energy_interface.electric_buffer_size = SpaceElevator.interface_energy_buffer
              struct.energy_interface.energy = math.max(struct.energy_interface.energy + struct.lua_energy / 2, 0)
              struct.opposite_struct.energy_interface.energy = math.max(
                struct.energy_interface.energy + struct.lua_energy / 2, 0)
              struct.lua_energy = 0;
            end
          end
          if struct.electric_pole and struct.opposite_struct.electric_pole then
            if struct.electric_pole.valid and struct.opposite_struct.electric_pole.valid then
              --remember connections
              local primary_connections = {}
              local secondary_connections = {}
              for _, wcon in pairs(struct.electric_pole.get_wire_connectors(true)) do
                for _,con in pairs(wcon.connections) do
                  table.insert(primary_connections, {target = con.target, origin = wcon.wire_connector_id, wire_type = con.origin})
                end
              end
              for _, wcon in pairs(struct.opposite_struct.electric_pole.get_wire_connectors(true)) do
                for _,con in pairs(wcon.connections) do
                  table.insert(secondary_connections, {target = con.target, origin = wcon.wire_connector_id, wire_type = con.origin})
                end
              end
              --destroy the poles
              struct.electric_pole.destroy();
              struct.opposite_struct.electric_pole.destroy()
              local new_p_pole = struct.surface.create_entity { --rebuild to get game to connect to nearest poles
                name = "se-space-elevator-energy-pole",
                position = struct.position, struct.position,
                direction = defines.direction.north,
                force = struct.force_name,
                create_build_effect_smoke = false
              }
              ---@cast new_p_pole -?
              local new_s_pole = struct.opposite_struct.surface.create_entity {
                name = "se-space-elevator-energy-pole",
                position = struct.opposite_struct.position,
                direction = defines.direction.north,
                force = struct.force_name,
                create_build_effect_smoke = false
              }
              ---@cast new_s_pole -?
              local new_switch = struct.surface.create_entity { -- the elevator now has a power switch too
                name = "se-space-elevator-power-switch",
                position = struct.position, struct.position,
                direction = defines.direction.north,
                force = struct.force_name,
                create_build_effect_smoke = false
              }
              ---@cast new_switch -?
              new_switch.power_switch_state = true
              struct.power_switch = new_switch
              struct.electric_pole = new_p_pole
              struct.opposite_struct.electric_pole = new_s_pole
              --reconnect circuit wires and copper wires
              for _, connect_info in pairs(primary_connections) do
                if connect_info.target.valid then
                  struct.electric_pole.get_wire_connector(connect_info.origin, true).connect_to(connect_info.target, false, connect_info.wire_type)
                end
              end
              for _, connect_info in pairs(secondary_connections) do
                if connect_info.target.valid then
                  struct.opposite_struct.electric_pole.get_wire_connector(connect_info.origin, true).connect_to(connect_info.target, false, connect_info.wire_type)
                end
              end
              --connect the ends together
              if struct.built then
                SpaceElevator.connect_wires(struct)
              else
                SpaceElevator.disconnect_wires(struct)
              end
            end
          end
        end
      end
      local tick_task = new_tick_task("game-message") --[[@as GameMessageTickTask]]
      tick_task.message =
      "[img=utility/warning_icon] [color=255,255,0]Space elevators [img=item/se-space-elevator] now require wired connections to transfer power.\nPlease check your elevators if you rely on them for power.[/color]"
      tick_task.delay_until = game.tick + 3 * 60;
    end

    -- Disable vanilla win
    if remote.interfaces["silo_script"] and remote.interfaces["silo_script"]["set_no_victory"] then
      remote.call("silo_script", "set_no_victory", true)
    end

    ---@class storage
    ---@field package rng any DEPRECIATED
    -- Renamed storage.rng to storage.universe_rng
    storage.universe_rng = storage.rng
    storage.rng = nil
  end,

  ["0.6.113"] = function()
    -- Fix zone controls for biter-less special zones sometimes still having vitamelange
    -- Only for non-generated surfaces, existing surrfaces can be fixed optionally with /se-migration-remove-vita-from-special-zones
    for _, zone in pairs(storage.zone_index) do
      if zone.special_type and zone.special_type ~= "vitamelange" and not zone.surface_index then
        zone.controls["se-vitamelange"] = {frequency = 1, size = 0, richness = 0}
      end
    end
  end,

  ["0.6.117"] = function()
    -- Recreate all core seams to fix potential rounding error
    -- https://forums.factorio.com/viewtopic.php?f=47&t=104138
    storage.core_seams_by_registration_number = storage.core_seams_by_registration_number or {}
    for _, zone in pairs(storage.zone_index) do
      if zone.type == "planet" or zone.type == "moon" then
        ---@cast zone PlanetType|MoonType
        if zone.fragment_name and prototypes.entity[zone.fragment_name] then
          CoreMiner.reset_seams(zone)
        end
      end
    end
  end,

  ["0.6.118"] = function()
    -- cache the watch areas and filters of exiting elevators
    if storage.space_elevators then
      for _, struct in pairs(storage.space_elevators) do
        struct.watch_area = Util.area_add_position(SpaceElevator.space_elevator_watch_rect[struct.direction], struct.position)
        struct.output_area = Util.area_add_position(SpaceElevator.space_elevator_output_rect[struct.direction], struct.position)
        struct.force_forward_area = Util.area_add_position(SpaceElevator.space_elevator_force_forward_rect, struct.position)
      end
    end

    -- make new storage for existing games, if necessary
    storage.cache_rocket_fuel_cost = storage.cache_rocket_fuel_cost or {}

    -- close all the GUIs impacted by the changes to refactor the dropdown / search into a common component
    -- not sure if it's necessary but it doesn't hurt to do so out of an abundance of caution
    for _, player in pairs(game.players) do
      if player then
        DeliveryCannonGUI.gui_close(player)
        LaunchpadGUI.gui_close(player)
        LandingpadGUI.gui_close(player)
        SpaceshipGUI.gui_close(player)
      end
    end

    -- Delivery cannons switch to mode based operation
    -- Converts existing is_off and auto_select_targets into modes
    -- This doesn't migrate entity tags on blueprints so deserializing blueprinted delivery cannons must maintain knowledge of these separate settings
    for _, delivery_cannon in pairs(storage.delivery_cannons) do
      delivery_cannon.mode = DeliveryCannon.mode_for_individual_settings(delivery_cannon.is_off, delivery_cannon.auto_select_targets)
    end
    -- Adds the new storage table for tracking delivery cannon artillery queues
    storage.delivery_cannon_queues = {}
  end,

  ["0.6.121"] = function()

    if script.active_mods["Krastorio2"] then

      game.print({"space-exploration.migrate_0_6_121"})

      -- Research techs for the Advanced Catalogue and Broad Advanced Catalogue if the Science Packs are already researched.
      for _, force in pairs(game.forces) do
        if force.technologies["kr-advanced-tech-card"].researched then
          force.technologies["se-kr-advanced-catalogue-1"].researched = true
        end
        if force.technologies["kr-singularity-tech-card"].researched then
          force.technologies["se-kr-advanced-catalogue-2"].researched = true
        end
      end

      -- Remove Gravimetrics facility entities performing the old Singularity Card recipes and place their contents in a box.
      for _, surface in pairs(game.surfaces) do
        local grav_facs = surface.find_entities_filtered{name="se-space-gravimetrics-laboratory"}
        for _, grav_fac in pairs(grav_facs) do
          local recipe = grav_fac.get_recipe()
          if recipe and (recipe.name == "kr-singularity-tech-card" or recipe.name == "singularity-tech-card-alt") then
            local position = table.deepcopy(grav_fac.position)
            local force = grav_fac.force
            local temp_inv = game.create_inventory(30)

            grav_fac.mine{
              inventory = temp_inv,
              raise_destroy = true
            }
            local migration_container = surface.create_entity{
              name="iron-chest",
              position=position,
              force=force,
              raise_built=true
            }
            if migration_container then
              local inventory = migration_container.get_inventory(defines.inventory.chest)
              if inventory then
                for _, item in pairs(temp_inv.get_contents()) do
                  inventory.insert({name=item.name, count=item.count, quality=item.quality})
                end
                temp_inv.destroy()
              else
                log("Migration failed for surface " .. surface.name .. " location x: " .. position.x .. " y: " .. position.y)
                for _, item in pairs(temp_inv.get_contents()) do
                  log("Item lost: " .. item.name .. " count: " .. item.count)
                end
              end
            end
          end
        end
      end

      -- Avoid deleting efficiency modules in new recipes that disallow them
      replace_dummy_recipes({"kr-atmospheric-condenser", "kr-electrolysis-plant"})
    end
  end,

  ["0.6.122"] = function()
    -- Avoid deleting efficiency modules in new recipes that disallow them
    if script.active_mods["Krastorio2"] then
      replace_dummy_recipes({"kr-atmospheric-condenser-_-waterless"})
    end

    -- spaceships could have docked but updated one last time causing them to think they were still flying
    for _, ss in pairs(storage.spaceships) do
      if not Spaceship.is_on_own_surface(ss) then
        -- if its docked, it shouldn't be flagged as moving
        if ss.speed ~= 0 or ss.is_moving then
          log("Fixing spaceship combinator: "..ss.name)
          ss.speed = 0
          ss.is_moving = false
          Spaceship.update_output_combinator(ss, game.tick)
        end
      end
    end

    -- find and re-register any landing or launch pads that may have been lost due to
    -- abandoning a spaceship with the same index as a zone
    for _, zone in pairs(storage.zone_index) do
      local surface = Zone.get_surface(zone)
      if surface then
        for _, entity in pairs(surface.find_entities_filtered{name=Landingpad.name_rocket_landing_pad}) do
          if not storage.rocket_landing_pads[entity.unit_number] then
            log("Re-registering landing pad on "..surface.name..":"..entity.unit_number)
            Landingpad.on_entity_created({
              entity = entity,
              tick = game.tick,
            })
          end
        end
        for _, entity in pairs(surface.find_entities_filtered{name=Launchpad.name_rocket_launch_pad}) do
          if not storage.rocket_launch_pads[entity.unit_number] then
            log("Re-registering launch pad on "..surface.name..":"..entity.unit_number)
            Launchpad.on_entity_created({
              entity = entity,
              tick = game.tick,
            })
          end
        end
      end
    end

    -- indestructible space elevator power switch
    for _, zone in pairs(storage.zone_index) do
      local surface = Zone.get_surface(zone)
      if surface then
        for _, entity in pairs(surface.find_entities_filtered{name="se-space-elevator-power-switch"}) do
          if entity.destructible then
            log("Marking power switch destructible on "..surface.name..": "..entity.unit_number)
            entity.destructible = false
          end
        end
      end
    end
  end,

  ["0.6.123"] = function()
    -- Correct special type "kr-imersite" being set when Krastorio2 is not active
    if not script.active_mods["Krastorio2"] then
      for _, zone in pairs(storage.zone_index) do
        if    zone.special_type
          and zone.special_type == "kr-imersite"
        then
          zone.special_type = nil
        end
      end
    end
  end,

  ["0.6.124"] = function ()
    -- Enable the Spaceship Victory tech when Krastorio 2 is enabled
    storage.tick_tasks = storage.tick_tasks or {}
    storage.next_tick_task_id = storage.next_tick_task_id or 1
    Krastorio2.enable_spaceship_victory_tech_on_migrate()
  end,

  ["0.6.127"] = function()
    --recondense any tables that might have holes in their index
    storage.spaceships = storage.spaceships or {}
    for _, spaceship in pairs(storage.spaceships) do
      if spaceship.particles then
        local particles = {}
        for _, particle in pairs(spaceship.particles) do
          table.insert(particles, particle)
        end
        spaceship.particles = particles
      end

      if spaceship.entity_particles then
        local entity_particles = {}
        for _, entity_particle in pairs(spaceship.entity_particles) do
          table.insert(entity_particles, entity_particle)
        end
        spaceship.entity_particles = entity_particles
      end

      if spaceship.engines then
        local engines = {}
        for _, engine in pairs(spaceship.engines) do
          table.insert(engines, engine)
        end
        spaceship.engines = engines
      end
    end

    if storage.simulation_spaceships then -- Sim save files
      for _, spaceship in pairs(storage.simulation_spaceships) do
        if spaceship.particles then
          local particles = {}
          for _, particle in pairs(spaceship.particles) do
            table.insert(particles, particle)
          end
          spaceship.particles = particles
        end

        if spaceship.entity_particles then
          local entity_particles = {}
          for _, entity_particle in pairs(spaceship.entity_particles) do
            table.insert(entity_particles, entity_particle)
          end
          spaceship.entity_particles = entity_particles
        end

        if spaceship.engines then
          local engines = {}
          for _, engine in pairs(spaceship.engines) do
            table.insert(engines, engine)
          end
          spaceship.engines = engines
        end
      end
    end

    -- Beacon overload optimizations
    storage.beacon_overloaded_entities = storage.beacon_overloaded_entities or {}
    storage.beacon_overloaded_shapes = storage.beacon_overloaded_shapes or {}
  end,

  ["0.6.130"] = function()
    -- Fix mismatched own_surface_index and own_surface
    for _, spaceship in pairs(storage.spaceships) do
      if spaceship.own_surface_index then
        spaceship.own_surface = game.get_surface(spaceship.own_surface_index)
      else
        spaceship.own_surface = nil
      end
    end
  end,

  ["0.6.131"] = function()
    -- storages moved to on_init
    storage.playerdata = storage.playerdata or {}
    storage.tick_tasks = storage.tick_tasks or {}
    storage.next_tick_task_id = storage.next_tick_task_id or 1
    storage.forces = storage.forces or {}
    storage.glyph_vaults = storage.glyph_vaults or {}
    storage.glyph_vaults_made_loot = storage.glyph_vaults_made_loot or {}
    storage.gravimetrics_labs = storage.gravimetrics_labs or {}
    storage.core_seams_by_registration_number = storage.core_seams_by_registration_number or {}
    storage.delivery_cannon_payloads = storage.delivery_cannon_payloads or {}
    storage.dimensional_anchors = storage.dimensional_anchors or {}
    storage.energy_beam_target_zones = storage.energy_beam_target_zones or {}
    storage.energy_transmitters = storage.energy_transmitters or {}
    storage.rocket_landing_pads = storage.rocket_landing_pads or {}
    storage.linked_containers = storage.linked_containers or {}
    storage.meteor_showers = storage.meteor_showers or {}
    storage.nexus = storage.nexus or {}
    storage.ruins = storage.ruins  or {}
    storage.space_elevators = storage.space_elevators or {}
    storage.spaceship_projectile_speeds = storage.spaceship_projectile_speeds or {}
    storage.spaceships = storage.spaceships or {}
    storage.train_gui_trains = storage.train_gui_trains or {}
    storage.zones_by_surface = storage.zones_by_surface or {}
    storage.cache_travel_delta_v = storage.cache_travel_delta_v or {}
    storage.chart_tag_buffer = storage.chart_tag_buffer or {}
    storage.chart_tag_next_id = storage.chart_tag_next_id or 0
    ---@class storage
    ---@field package next_meteor_shower any DEPRECIATED
    storage.next_meteor_shower = nil
    ---@class storage
    ---@field package astronomical any DEPRECIATED
    storage.astronomical = nil
    storage.space_capsules = storage.space_capsules or {}
    storage.vault_loot_rng = storage.vault_loot_rng or game.create_random_generator()
    storage.next_spaceship_index = storage.next_spaceship_index or 1
    if not storage.core_mining then CoreMiner.create_core_mining_tables() end

    -- migrate arty40 entities
    if storage.ruins and storage.ruins["arty40"] then
      local global_ruins = storage.ruins["arty40"]
      global_ruins.accumulators = global_ruins.accumulators or {}
      global_ruins.logistic_chest_buffers = global_ruins.logistic_chest_buffers or {}
      if global_ruins.zone_index then
        local zone = Zone.from_zone_index(global_ruins.zone_index)
        if zone and zone.surface_index then
          local surface = game.get_surface(zone.surface_index)
          if surface then
            local accumulators = surface.find_entities_filtered{type="accumulator", force="conquest"}
            for _, entity in pairs(accumulators) do
              global_ruins.accumulators[entity] = true
            end
            local buffers = surface.find_entities_filtered{name="buffer-chest", force="conquest"}
            for _, entity in pairs(buffers) do
              global_ruins.logistic_chest_buffers[entity] = true
            end
            local tanks = surface.find_entities_filtered{name="storage-tank", force="conquest"}
            global_ruins.petrol_tank = tanks[1]
          end
        end
      end
    end

    -- Fix typoed `unit_unber` reference
    if storage.gravimetrics_labs then
      for _, lab in pairs(storage.gravimetrics_labs) do
        ---@class GravimetricsLabInfo
        ---@field package unit_unber any DEPRECATED
        if lab.unit_unber then
          lab.unit_number = lab.unit_unber
          lab.unit_unber = nil
        end
      end
    end
    if storage.nexus then
      for _, nexus in pairs(storage.nexus) do
        ---@class NexusInfo
        ---@field package unit_unber any DEPRECATED
        if nexus.unit_unber then
          nexus.unit_number = nexus.unit_unber
          nexus.unit_unber = nil
        end
      end
    end

    -- Fix arty40 losing its tech
    if storage.ruins and storage.ruins["arty40"] then
      if Ruin.ruins["arty40"] and Ruin.ruins["arty40"].prebuild then
        Ruin.ruins["arty40"].prebuild()
      end
    end
  end,

  ["0.6.132"] = function()
    -- Reset conquest tech (could be inflated from recreating the ruin multiple times)
    local conquest = game.forces["conquest"]
    conquest.reset() -- Will trigger SystemForces.setup_system_forces()
  end,

  ["0.6.134"] = function()
    --Init `storage.train_gui_trains` due to games being started without it.
    storage.train_gui_trains = storage.train_gui_trains or {}
  end,

  ["0.6.135"] = function()
    local meteor_names = {
      "se-static-meteor-01",
      "se-static-meteor-02",
      "se-static-meteor-03",
      "se-static-meteor-04",
      "se-static-meteor-05",
      "se-static-meteor-06",
      "se-static-meteor-07",
      "se-static-meteor-08",
      "se-static-meteor-09",
      "se-static-meteor-10",
      "se-static-meteor-11",
      "se-static-meteor-12",
      "se-static-meteor-13",
      "se-static-meteor-14",
      "se-static-meteor-15",
      "se-static-meteor-16",
    }

    -- Mark any meteors that collide with player structures for deconstruction
    for _, surface in pairs(game.surfaces) do
      for _, meteor in pairs(surface.find_entities_filtered{name=meteor_names}) do
        colliding_area = Util.area_add_position(meteor.prototype.collision_box, meteor.position)
        for _, entity in pairs(surface.find_entities_filtered{area=colliding_area}) do
            if not SystemForces.is_system_force(entity.force.name) then
              meteor.order_deconstruction(entity.force)
              break
            end
        end
      end
    end

    -- Clean up any orphan poles that might be stuck to spaceships. This will not fix broken
    -- clamps themselves. The player will have to pick it up and place it down again.
    for _, surface in pairs(game.surfaces) do
      for _, pole in pairs(surface.find_entities_filtered{ name = {
        "se-spaceship-clamp-power-pole-external-east",
        "se-spaceship-clamp-power-pole-external-west"
      }}) do
        if not surface.find_entity("se-spaceship-clamp", pole.position) then
          pole.destroy()
        end
      end
    end

    for _, spaceships in pairs({
      storage.spaceships or {},
      storage.simulation_spaceships or { },
    }) do
      for _, spaceship in pairs(spaceships) do
        if spaceship.console and spaceship.console.valid then
          -- Spaceships that do not have a console can only happen if the mod
          -- is updated after a landed console is destroyed, and before its next update.
          -- This case is handled in the `on_post_entity_died` event.
          spaceship.last_console_unit_number = spaceship.console.unit_number
        end
      end
    end
  end,

  ["0.6.139"] = function()
    -- Spaceship scheduler

    if not storage.spaceship_scheduler_groups then
      -- Create some missing storages
      storage.spaceship_scheduler_groups = storage.spaceship_scheduler_groups or {}
      storage.spaceship_scheduler_interrupts = storage.spaceship_scheduler_interrupts or {}

      for _, spaceship in pairs(storage.spaceships or {}) do
        -- Add scheduler to all existing spaceships
        if not spaceship.scheduler then
          SpaceshipScheduler.add_to_spaceship(spaceship)
        end
      end

      -- Add new fields to spaceship clamps
      for _, clamp_info in pairs(storage.spaceship_clamps or {}) do
        local clamp = clamp_info.main
        if not (clamp and clamp.valid) then
          -- We need the entity to be valid to determine the dirtion.
          -- And it seems like the cleaning of invalid clamps wasn't done before.
          -- (It is now though in the clamp's on_tick)
          -- The detroy function expects the reservation table to exist
          if not clamp_info.spaceship_reservations then clamp_info.spaceship_reservations = { } end
          SpaceshipClamp.destroy(clamp_info)
        else
          clamp_info.direction = clamp.direction
          if not clamp_info.priority then clamp_info.priority = SpaceshipClamp.default_priority end
          if not clamp_info.tag then clamp_info.tag = SpaceshipClamp.find_unique_tag(clamp) end
          if not clamp_info.spaceship_reservations then clamp_info.spaceship_reservations = { } end
        end
      end

      -- Close all spaceship GUIs to force a refresh. This will also stop
      -- any anchor-scouting in progress.
      for _, player in pairs(game.players) do
        SpaceshipGUI.gui_close(player)
      end
    end
  end,

  ["0.6.144"] = function()
    -- Fix corrupt canary
    -- If the canary was captured between v0.6.139 and 0.6.143 (inclusive) then the scheduler
    -- won't be connected to an actual scheduler group. This is because the force of the spaceship
    -- was changed without updating the schedule groups.

    ---@param spaceship SpaceshipType
    ---@return boolean?
    local is_probably_broken_canary = function(spaceship)
      if not storage.spaceship_scheduler_groups[spaceship.force_name] then return true end
      return storage.spaceship_scheduler_groups[spaceship.force_name][spaceship.scheduler.schedule_group_name] == nil
    end

    for _, spaceship in pairs(storage.spaceships) do
      if is_probably_broken_canary(spaceship) then

        -- Clean up old schedule group
        if storage.spaceship_scheduler_groups["capture"] then
          storage.spaceship_scheduler_groups["capture"][spaceship.scheduler.schedule_group_name] = nil
        end

        -- Recreate new schedule group cleanly
        SpaceshipScheduler.add_to_spaceship(spaceship)
      end
    end
  end,

  ["0.6.146"] = function()
    -- get position for migration to 0.7
    for _, landingpad in pairs(storage.rocket_landing_pads or {}) do
      -- Landing pads will not be valid if directly migrating from pre 0.6.146
      -- to post 0.7 because type of the entity changed during the json migration.
      -- So the warning in the 0.7 lua migration will be triggered when required.
      if landingpad.container.valid then
        landingpad.position = landingpad.container.position
      end
    end
  end,

  ["0.7.0"] = function()

    --["update-directions-define"] = function()
      -- In Factorio 2.0 the defines.direction changed from 8 to 16.
      local updated_direction = {
        [0] = defines.direction.north,
        [2] = defines.direction.east,
        [4] = defines.direction.south,
        [6] = defines.direction.west,
      }

      ---@param direction defines.direction
      ---@return defines.direction new_direction
      local function get_new_direction(direction)
        local new_direction = updated_direction[direction]
        assert(new_direction, "Unsupported old direction: "..direction)
        return new_direction
      end

      -- ELEVATORS ------------------------------------
      for _, elevator in pairs(storage.space_elevators) do
        elevator.direction = get_new_direction(elevator.direction)
      end

      -- CLAMPS ------------------------------------
      local fix_clamp_directions = false
      for _, clamp_info in pairs(storage.spaceship_clamps) do
        if clamp_info.direction ~= defines.direction.east and clamp_info.direction ~= defines.direction.west then
          -- We found a clamp with an old direction
          -- Therefore the 0.6.139 migration was already run, and we need to fix the directions
          fix_clamp_directions = true
          break
        end
      end
      if fix_clamp_directions then
        for _, clamp_info in pairs(storage.spaceship_clamps) do
          clamp_info.direction = get_new_direction(clamp_info.direction)
        end
      end

      -- SPACESHIP SCHEDULER ---------------------------------
      ---@param record SpaceshipSchedulerRecord
      local function update_scheduler_record(record)
        if record.spaceship_clamp and record.spaceship_clamp.direction then
          record.spaceship_clamp.direction = get_new_direction(record.spaceship_clamp.direction)
        end
        if record.destination_descriptor and record.destination_descriptor.clamp and record.destination_descriptor.clamp.direction then
          record.destination_descriptor.clamp.direction = get_new_direction(record.destination_descriptor.clamp.direction)
        end
      end

      for _, force_groups in pairs(storage.spaceship_scheduler_groups) do
        for _, group in pairs(force_groups) do
          for _, record in pairs(group.schedule) do
            update_scheduler_record(record)
          end
        end
      end
      for _, force_interrupts in pairs(storage.spaceship_scheduler_interrupts) do
        for _, interrupt in pairs(force_interrupts) do
          for _, target in pairs(interrupt.targets) do
            update_scheduler_record(target)
          end
        end
      end
      for _, spaceship in pairs(storage.spaceships) do
        for _, target in pairs(spaceship.scheduler.current_interrupts or { }) do
          update_scheduler_record(target)
        end
      end
    --end,

    --["render-objects"] = function()
      if storage.gate then
        if storage.gate.void_sprite then
          storage.gate.void_sprite = rendering.get_object_by_id(storage.gate.void_sprite)
        end
        if storage.gate.activation_fx then
          storage.gate.activation_fx.cloud_1 = rendering.get_object_by_id(storage.gate.activation_fx.cloud_1)
          storage.gate.activation_fx.cloud_2 = rendering.get_object_by_id(storage.gate.activation_fx.cloud_2)
        end
      end

      if storage.interburbulator then
        storage.interburbulator.light = rendering.get_object_by_id(storage.interburbulator.light)
        storage.interburbulator.grid = rendering.get_object_by_id(storage.interburbulator.grid)
        storage.interburbulator.robot_text = rendering.get_object_by_id(storage.interburbulator.robot_text)
      end

      for zone_index, dimensional_anchor in pairs(storage.dimensional_anchors) do
        dimensional_anchor.low_power_icon = rendering.get_object_by_id(dimensional_anchor.low_power_icon)
      end

      for unit_number, linked_container in pairs(storage.linked_containers) do
        if linked_container.text_id then
          ---@class LinkedContainerInfo
          ---@field package text_id any DEPRECATED
          linked_container.text_render = rendering.get_object_by_id(linked_container.text_id)
          linked_container.text_id = nil
        end
        if linked_container.effect_id then
          ---@class LinkedContainerInfo
          ---@field package effect_id any DEPRECATED
          linked_container.effect_render = rendering.get_object_by_id(linked_container.effect_id)
          linked_container.effect_id = nil
        end
      end

      for _, tick_task in pairs(storage.tick_tasks) do
        ---@cast tick_task SolarFlareTickTask
        if tick_task.beams then
          for _, beam in pairs(tick_task.beams) do
            ---@class SolarFlareBeamInfo
            ---@field package beam_sprite_id any DEPRECATED
            if beam.beam_sprite_id then
              beam.beam_sprite_render = rendering.get_object_by_id(beam.beam_sprite_id)
            end
          end
        end
      end

      for unit_number, tree in pairs(storage.energy_transmitters) do
        if tree.glaive_beam_sprite_id then
          ---@class EnergyBeamEmitterInfo
          ---@field package glaive_beam_sprite_id any DEPRECATED
          tree.glaive_beam_sprite_render = rendering.get_object_by_id(tree.glaive_beam_sprite_id)
          tree.glaive_beam_sprite_id = nil
        end
      end

      for unit_number, struct in pairs(storage.space_elevators) do
        if struct.text then
          struct.text = rendering.get_object_by_id(struct.text)
        end
        if struct.icon then
          struct.icon = rendering.get_object_by_id(struct.icon)
        end
      end

      for _, defences in pairs({storage.meteor_defences, storage.meteor_point_defences}) do
        for unit_number, defence in pairs(defences) do
          ---@class MeteorDefenceInfo
          ---@field package charging_shape_id any DEPRECATED
          ---@field package charging_text_id any DEPRECATED
          if defence.charging_shape_id then
            defence.charging_shape = rendering.get_object_by_id(defence.charging_shape_id)
            defence.charging_shape_id = nil
          end
          if defence.charging_text_id then
            defence.charging_text = rendering.get_object_by_id(defence.charging_text_id)
            defence.charging_text_id = nil
          end
        end
      end

      for unit_number, nexus in pairs(storage.nexus) do
        ---@class NexusInfo
        ---@field package countdown_id any DEPRECATED
        if nexus.countdown_id then
          nexus.countdown_render = rendering.get_object_by_id(nexus.countdown_id)
          nexus.countdown_id = nil
        end
      end

      for force_name, forcedata in pairs(storage.forces) do
        if forcedata.zone_assets then
          for zone_index, zone_assets in pairs(forcedata.zone_assets) do
            if zone_assets.energy_beam_defence then
              for unit_number, defence in pairs(zone_assets.energy_beam_defence) do
                if defence.glow_id then
                  defence.glow_sprite = rendering.get_object_by_id(defence.glow_id)
                end
              end
            end
          end
        end
      end

      for player_id, playerdata in pairs(storage.playerdata) do
        if playerdata.map_view_objects then
          for i, object in pairs(playerdata.map_view_objects) do
            playerdata.map_view_objects[i] = rendering.get_object_by_id(object)
          end
        end
        if playerdata.capture_text then
          playerdata.capture_text = rendering.get_object_by_id(playerdata.capture_text)
        end
      end

      for player_id, connected_playerdata in pairs(storage.connected_players_in_remote_view) do
        if connected_playerdata.satellite_light then
          connected_playerdata.satellite_light = rendering.get_object_by_id(connected_playerdata.satellite_light)
        end
      end

      ---@class SpaceshipType
      ---@field package particle_object_ids any DEPRECATED
      for _, spaceship in pairs(storage.spaceships) do
        if spaceship.particle_object_ids then
          spaceship.particle_objects = {}
          for anchor_unit_number, particle_object_ids in pairs(spaceship.particle_object_ids) do
            spaceship.particle_objects[anchor_unit_number] = {}
            for _, particle_object_id in pairs(particle_object_ids) do
              table.insert(spaceship.particle_objects[anchor_unit_number], rendering.get_object_by_id(particle_object_id))
            end
          end
          spaceship.particle_object_ids = nil
        end
      end
    --end,

    --["spaceship-updates"] = function()
      for _, spaceship in pairs(storage.spaceships) do
        ---@class SpaceshipType
        ---@field package circuits_to_restore any DEPRECATED
        ---@field package circuitstore_phase any DEPRECATED
        ---@field package circuits_to_restore_tick any DEPRECATED
        spaceship.circuits_to_restore = nil
        spaceship.circuitstore_phase = nil
        spaceship.circuits_to_restore_tick = nil
      end
    --end,

    --["uninvert-enemy-base-controls"] = function()
      -- Remove negative enemy base settings bacause it spawns enemies in F2.0
      for _, zone in pairs(storage.zone_index) do
        for _, control in pairs(zone.controls or {}) do
          if control.frequency and control.frequency <= 0 then
            control.frequency = 1
            control.size = 0
            control.richness = 0
          end
        end

       if zone.controls and zone.controls["enemy-base"] then
          if zone.controls["enemy-base"].size < 0 then
            zone.controls["enemy-base"].size = 0
          end
          if zone.controls["enemy-base"].richness < 0 then
            zone.controls["enemy-base"].richness = 0
          end
        end
      end
      for _, surface in pairs(game.surfaces) do
        local map_gen_settings = surface.map_gen_settings
        --Fix all mapgensettings with 0 frequencies
        for _, control in pairs(map_gen_settings.autoplace_controls or {}) do
          if control.frequency and control.frequency <= 0 then
            control.frequency = 1
            control.size = 0
            control.richness = 0
          end
        end
        if map_gen_settings.cliff_settings then
          if    map_gen_settings.cliff_settings.cliff_elevation_0
            and map_gen_settings.cliff_settings.cliff_elevation_0 == math.huge
          then
            map_gen_settings.cliff_settings.cliff_elevation_0 = 10
          end
          if    map_gen_settings.cliff_settings.cliff_elevation_interval
            and map_gen_settings.cliff_settings.cliff_elevation_interval == math.huge
          then
            map_gen_settings.cliff_settings.cliff_elevation_interval = 400
          end
        end
        surface.map_gen_settings = map_gen_settings

        if map_gen_settings.autoplace_controls and map_gen_settings.autoplace_controls["enemy-base"] then
          local changed = false
          if map_gen_settings.autoplace_controls["enemy-base"].size < 0 then
            map_gen_settings.autoplace_controls["enemy-base"].size = 0
            changed = true
          end
          if map_gen_settings.autoplace_controls["enemy-base"].richness < 0 then
            map_gen_settings.autoplace_controls["enemy-base"].richness = 0
            changed = true
          end
          if changed then
            surface.map_gen_settings = map_gen_settings
          end
        end
      end
    --end,

    --["restrict-planet-autoplace"] = function()
      for _, zone in pairs(storage.zone_index) do
        local surface = Zone.get_surface(zone)
        if surface then
          local map_gen_settings = surface.map_gen_settings
          if Zone.is_space(zone) then
            Zone.set_autoplace_settings_for_space(zone, map_gen_settings)
          else
            Zone.set_autoplace_settings_for_solid(zone, map_gen_settings)
          end
          if map_gen_settings.cliff_settings.cliff_elevation_0 == math.huge then
            map_gen_settings.cliff_settings.cliff_elevation_0 = 0
          end
          if map_gen_settings.cliff_settings.cliff_elevation_interval == math.huge then
            map_gen_settings.cliff_settings.cliff_elevation_interval = 0
          end
          surface.map_gen_settings = map_gen_settings
        end
      end
    --end,

    --["remote-view"] = function()
      for _, player_in_remote_view in pairs(storage.connected_players_in_remote_view) do
        if player_in_remote_view.satellite_light and type(player_in_remote_view.satellite_light) == "number" then
          local light = rendering.get_object_by_id(player_in_remote_view.satellite_light)
          if light then light.destroy() end
        end
        player_in_remote_view.satellite_light = nil

        -- Force player back to character controller.
        local player = player_in_remote_view.player
        local player_data = storage.playerdata[player.index]
        player.teleport(player_data.character.position, player_data.character.surface)
        player.set_controller{
          type = defines.controllers.character,
          character = player_data.character,
        }
      end

      for player_index, player_data in pairs(storage.playerdata) do

        -- IMORTANT: set to the correct permissions group
        local player = game.players[player_index]
        if player then
          if player_data.pre_nav_permission_group and player_data.pre_nav_permission_group.valid and player_data.pre_nav_permission_group.name ~= RemoteView.name_permission_group then
            player.permission_group = player_data.pre_nav_permission_group
          else
            player.permission_group = nil
          end
        end

        -- In 1.1 Nav Sat used the cheat mode, we don't anymore.
        -- Remove it from the player depending on the saved value.
        player.cheat_mode = player_data.saved_cheat_mode or false

        ---@class PlayerData
        ---@field package pre_nav_permission_group any DEPRECATED
        ---@field package saved_cheat_mode any DEPRECATED
        ---@field package remote_view_active_map any DEPRECATED
        player_data.pre_nav_permission_group = nil
        player_data.saved_cheat_mode = nil
        player_data.remote_view_active_map = nil
      end

      for _, player in pairs(game.players) do
        RemoteView.stop(player)
      end

      -- Clean up unused permissions groups.
      for _, permission_group in pairs(game.permissions.groups) do
        if permission_group.name == "satellite" or string.ends(permission_group.name, "_satellite") then
          permission_group.destroy()
        end
      end

      -- Remove naview top gui button if it exists
      for _, player in pairs(game.players) do
        local gui = player.gui.top["mod_gui_top_frame"]
        if gui then
          gui = gui["mod_gui_inner_frame"]
          if gui then
            gui = gui["se-overhead_satellite"]
            if gui then
              gui.destroy()
            end
          end
        end
      end
    --end,

    --["track-basic-rocket-silos"] = function()
      storage.basic_rocket_silos = {}
      for _, surface in pairs(game.surfaces) do
        for _, entity in pairs(surface.find_entities_filtered{name = Rocketsilo.name_rocket_silos}) do
          Rocketsilo.on_entity_created({entity = entity})
        end
      end

      local warned = false
      local old_landing_pads = util.shallow_copy(storage.rocket_landing_pads)
      for old_unit_number, landingpad in pairs(old_landing_pads) do
        local found = false
        if not landingpad.position then
          if not warned then
            warned = true
            game.print("[color=red]Missing landingpad position data. The game must be saved with a more recent version of SE0.6 on Factorio 1.1 before you load it in SE0.7.x on Factorio 2.0 otherwise the replaced landingpads won't have their old names.[/color]")
          end
        else
          local surface = Zone.get_surface(landingpad.zone)
          local entity = surface.find_entity(Landingpad.name_rocket_landing_pad, landingpad.position)
          if entity then
            found = true
            Landingpad.remove_struct_from_tables(landingpad) -- first
            storage.rocket_landing_pads[landingpad.unit_number] = nil
            storage.rocket_landing_pads[entity.unit_number] = landingpad
            landingpad.container = entity
            landingpad.unit_number = entity.unit_number
            Landingpad.name(landingpad)
            --Log.debug("Landingpad " .. landingpad.name .. " found at position " .. landingpad.position.x .. " " .. landingpad.position.y)
          else
            Log.debug("Landingpad " .. landingpad.name .. " not found at position " .. landingpad.position.x .. " " .. landingpad.position.y)
          end
        end
        if not found then
          Landingpad.destroy(landingpad)
        end
      end

      -- Make sure any remaining landingpads are captured
      for _, surface in pairs(game.surfaces) do
        for _, entity in pairs(surface.find_entities_filtered{name = Landingpad.name_rocket_landing_pad}) do
          Landingpad.on_entity_created({entity = entity})
        end
      end
    --end,

    --["migration vault loot bags"] = function()
      --effectivity modules are now named efficiency modules
      for _, forcedata in pairs(storage.forces) do
        for i, loot in pairs(forcedata.vaults_loot_bag or {}) do
          if loot == "effectivity-module-9" then
            forcedata.vaults_loot_bag[i] = "efficiency-module-9"
          end
        end
      end
    --end,

    --migrate Factorio 1.1 lua rocket inventories to Factorio 2.0
    for _, struct in pairs(storage.rocket_launch_pads or {}) do
      if struct.launched_contents and not struct.launched_contents[1] then
        --Factorio 1.1 was a dictionary, 2.0 is an array
        local launched_contents = {}
        for item, count in pairs(struct.launched_contents) do
          table.insert(launched_contents, {name=item, count = count, quality="normal"})
        end
        struct.launched_contents = launched_contents
      end
    end

    --migrate Factorio 1.1 lua rocket inventories to Factorio 2.0
    for _, tick_task in pairs(storage.tick_tasks or {}) do
      if tick_task.launched_contents and not tick_task.launched_contents[1] then
        ---@cast tick_task CargoRocketTickTask
        --Factorio 1.1 was a dictionary, 2.0 is an array
        local launched_contents = {}
        for item, count in pairs(tick_task.launched_contents) do
          table.insert(launched_contents, {name=item, count = count, quality="normal"})
        end
        tick_task.launched_contents = launched_contents
      end
    end
  end,

  ["0.7.8"] = function()
    --moves existing rocket launchpad seats to match new spawn location
    for _, forcedata in pairs(storage.forces or {}) do
      for _, zone_assets in pairs(forcedata.zone_assets or {}) do
        for _, launchpad_name in pairs(zone_assets.rocket_launch_pad_names or {}) do
          for _, launchpad_id in pairs(launchpad_name or {}) do
            for _, seat in pairs(launchpad_id.seats) do
              seat.teleport({seat.position.x, launchpad_id.silo.position.y + Launchpad.seat_y_offset})
              seat.orientation = 0.25
            end
          end
        end
      end
    end
  end,

  ["0.7.13"] = function()
    -- Fixes all clamps that might have had their circuit ids corrupted
    for _, clamp_info in pairs(storage.spaceship_clamps) do
      local entity = clamp_info.main
      if entity and entity.valid then
        SpaceshipClamp.validate_clamp_signal(clamp_info.main)
      end
    end

    --rebuild moveable train stop selector gui
    storage.train_gui_opened = storage.train_gui_opened or {}
    for _, player in pairs(game.players) do
      if player.gui.relative[TrainGUI.name_train_gui_root] then
        player.gui.relative[TrainGUI.name_train_gui_root].destroy()
      end
    end
  end,

  ["0.7.14"] = function()
    if storage.glyph_vaults then
      for _, vault in pairs(storage.glyph_vaults) do
        for vault, vault_infos in pairs(vault) do
          if vault_infos.surface_index then
            local surface = game.get_surface(vault_infos.surface_index)
            if surface then game.forces["enemy"].set_evolution_factor(1, surface.name) end
          end
        end
      end
    end
  end,

  ["0.7.17"] = function()
    for _, landing_pad in pairs(storage.rocket_landing_pads or {}) do
      local entity = landing_pad.container
      if entity and entity.valid then
        -- Disable any interaction with the logistics network
        for _, logistic_point in pairs(entity.get_logistic_point() or {}) do
          logistic_point.enabled = false
        end

        -- Make sure they are aligned to the 2x2 grid. The might not be after
        -- the migration from 1.1 to 2.0, and it really messes with the graphics
        -- Using `round` here move it makes the new 8x8 remain within the 9x9
        -- footprint of the old 1.1 landing pads.
        entity.teleport({
          math.round(entity.position.x / 2) * 2,
          math.round(entity.position.y / 2) * 2
        })
        end
    end
  end,

  ["0.7.18"] = function()
    for _, surface in pairs(game.surfaces) do
      for turbine_name, config in pairs(CondenserTurbine.turbine_configs) do
        -- First remove all orphan generators and tanks
        local destroy_if_orphan = function(entity)
          if not surface.find_entity(turbine_name, entity.position) then
            entity.destroy()
          end
        end
        for _, tank in pairs(surface.find_entities_filtered{name=config.tank_name}) do
          destroy_if_orphan(tank)
        end
        for _, generator in pairs(surface.find_entities_filtered{name=config.generator_names}) do
          destroy_if_orphan(generator)
        end

        -- Fix all turbines that might have their hidden tanks at the correct direction,
        -- and make the rotatable flag is set correctly
        for _, turbine in pairs(surface.find_entities_filtered{name=turbine_name}) do
          -- Ensure rotatable flag is correct
          turbine.rotatable = config.rotatable

          -- Ensure the tank is in the correct position and direction
          for _, tank in pairs(turbine.surface.find_entities_filtered({
            name = config.tank_name,
            area = turbine.bounding_box,
            limit = 1
          })) do
            tank.teleport(CondenserTurbine.determine_tank_position(turbine, config))
            tank.direction = turbine.direction
          end

          -- The big turbines might have their generators pointing the wrong way. Let's fix that.
          if turbine_name == "se-big-turbine" then
            local old_generator = CondenserTurbine.find_generator(turbine)
            if old_generator and old_generator.name ~= config.get_generator_name(turbine) then
              -- Delete NW entity
              local fluid_count = old_generator.remove_fluid({name = "se-decompressing-steam", amount = 10000})
              old_generator.destroy()

              -- Create new entity
              local new_generator = surface.create_entity({
                name = config.get_generator_name(turbine),
                position = turbine.position,
                force = turbine.force,
                direction = turbine.direction,
              })
              ---@cast new_generator -?
              new_generator.destructible = false
              if fluid_count ~= 0 then
                new_generator.insert_fluid({name = "se-decompressing-steam", amount = math.abs(fluid_count), temperature = 5000})
              end
            end
          end
        end
      end
    end

    for _, surface in pairs(game.surfaces) do
      local map_gen_settings = surface.map_gen_settings
      --Fix all mapgensettings with 0 frequencies
      for _, control in pairs(map_gen_settings.autoplace_controls or {}) do
        if control.frequency <= 0 then
          control.frequency = 1
          control.size = 0
          control.richness = 0
        end
      end
      surface.map_gen_settings = map_gen_settings
      for _, zone in pairs(storage.zone_index) do
        for _, control in pairs(zone.controls or {}) do
          if control.frequency and control.frequency <= 0 then
            control.frequency = 1
            control.size = 0
            control.richness = 0
          end
        end
      end
    end

    -- Update all spaceships with a localized surface name
    for _, spaceship in pairs(storage.spaceships) do
      Spaceship.rename(spaceship, spaceship.name)
    end
  end,

  ["0.7.20"] = function()
    local res_tbl = load("return "..storage.resources_and_controls_compare_string)()

    for index, fragment in pairs(res_tbl.core_fragments) do
      if fragment == "se-core-fragment-imersite" then
        res_tbl.core_fragments[index] = "se-core-fragment-kr-imersite"
      end
      if fragment == "se-core-fragment-mineral-water" then
        res_tbl.core_fragments[index] = "se-core-fragment-kr-mineral-water"
      end
      if fragment == "se-core-fragment-rare-metals" then
        res_tbl.core_fragments[index] = "se-core-fragment-kr-rare-metal-ore"
      end
    end
    table.sort(res_tbl.core_fragments)

    for index, control_name in pairs(res_tbl.resource_controls) do
      if control_name == "imersite" then
        res_tbl.resource_controls[index] = "kr-imersite"
      end
      if control_name == "mineral-water" then
        res_tbl.resource_controls[index] = "kr-mineral-water"
      end
      if control_name == "rare-metals" then
        res_tbl.resource_controls[index] = "kr-rare-metal-ore"
      end
    end

    for key, value in pairs(res_tbl.resource_settings) do
      if key == "imersite" then
        value.core_fragment = "se-core-fragment-kr-imersite"
        value.name = "kr-imersite"
        res_tbl.resource_settings["kr-imersite"] = value
        res_tbl.resource_settings[key] = nil
      end
      if key == "mineral-water" then
        value.core_fragment = "se-core-fragment-kr-mineral-water"
        value.name = "kr-mineral-water"
        res_tbl.resource_settings["kr-mineral-water"] = value
        res_tbl.resource_settings[key] = nil
      end
      if key == "rare-metals" then
        value.core_fragment = "se-core-fragment-kr-rare-metal-ore"
        value.name = "kr-rare-metal-ore"
        res_tbl.resource_settings["kr-rare-metal-ore"] = value
        res_tbl.resource_settings[key] = nil
      end
    end

    storage.resources_and_controls_compare_string = util.table_to_string(res_tbl)

    for _, zone in pairs(storage.zone_index) do
      if zone.controls then
        if zone.controls["imersite"] then
          zone.controls["kr-imersite"] = table.deepcopy(zone.controls["imersite"])
          zone.controls["imersite"] = nil
        end
        if zone.controls["mineral-water"] then
          zone.controls["kr-mineral-water"] = table.deepcopy(zone.controls["mineral-water"])
          zone.controls["mineral-water"] = nil
        end
        if zone.controls["rare-metals"] then
          zone.controls["kr-rare-metal-ore"] = table.deepcopy(zone.controls["rare-metals"])
          zone.controls["rare-metals"] = nil
        end
      end
      if zone.primary_resource then
        if zone.primary_resource == "imersite" then
          zone.primary_resource = "kr-imersite"
        end
        if zone.primary_resource == "mineral-water" then
          zone.primary_resource = "kr-mineral-water"
        end
        if zone.primary_resource == "rare-metals" then
          zone.primary_resource = "kr-rare-metal-ore"
        end
      end
      if zone.fragment_name then
        if zone.fragment_name == "se-core-fragment-imersite" then
          zone.fragment_name = "se-core-fragment-kr-imersite"
        end
        if zone.fragment_name == "se-core-fragment-mineral-water" then
          zone.fragment_name = "se-core-fragment-kr-mineral-water"
        end
        if zone.fragment_name == "se-core-fragment-rare-metals" then
          zone.fragment_name = "se-core-fragment-kr-rare-metal-ore"
        end
      end
    end
    ---@class storage
    ---@field package skip_resource_removal boolean Used only for migrations 0.6 -> 0.7

    storage.skip_resource_removal = true -- only used in 0.7.30 resource removal fix
  end,

  ["0.7.28"] = function()
    -- Stop special resources from appearing in migrated saves.
    for _, zone in pairs(storage.zone_index) do
      local surface = Zone.get_surface(zone)
      if surface then
        local map_gen_settings = surface.map_gen_settings
        if Zone.is_space(zone) then
          Zone.set_autoplace_settings_for_space(zone, map_gen_settings)
        else
          Zone.set_autoplace_settings_for_solid(zone, map_gen_settings)
        end
        surface.map_gen_settings = map_gen_settings
      end
    end
  end,

  ["0.7.30"] = function()
    -- in versions of 0.7 pre 0.7.28 there was the potential for resources to be added to autoplace_settings on surfaces where they shouldn't be
    -- if the version of "0.7.20" runs with skip_resource_removal, then the save is safe, but if it was run before "0.7.28", then the save may be contaminated
    -- so if storage.skip_resource_removal is not set then remove reosurces
    if not storage.skip_resource_removal then

      for _, zone in pairs(storage.zone_index) do
        local surface = Zone.get_surface(zone)
        if surface then
          local map_gen_settings = surface.map_gen_settings
          local resource_names = {}
          if Zone.is_space(zone) then
            -- remove space reosurces.
            resource_names ={"copper-ore", "iron-ore", "uranium-ore", "stone", "se-beryllium-ore", "se-water-ice", "se-methane-ice", "se-naquium-ore"}
          else
            -- remove land reosurces.
            resource_names = {"coal", "copper-ore", "iron-ore", "uranium-ore", "stone", "crude-oil",
            "se-vulcanite", "se-cryonite", "se-holmium-ore", "se-iridium-ore", "se-beryllium-ore", "se-vitamelange"}
          end
          local resources_to_remove = {}
          for _, resource_name in pairs(resource_names) do
            if not(map_gen_settings.autoplace_controls[resource_name] and map_gen_settings.autoplace_controls[resource_name].size > 0) then
              table.insert(resources_to_remove, resource_name)
            end
          end
          for _, entity in pairs(surface.find_entities_filtered{name = resources_to_remove}) do
            entity.destroy()
          end
        end
      end

    end
    storage.skip_resource_removal = nil -- clean up temp flag
  end,

  ["0.7.33"] = function()
    --Fix any potentially holey storage.delivery_cannon_payloads tables
    if not storage.delivery_cannon_payloads then return end
    local old_payloads = storage.delivery_cannon_payloads
    storage.delivery_cannon_payloads = {}
    for _, payload in pairs(old_payloads) do
      table.insert(storage.delivery_cannon_payloads, payload)
    end

    --remove orphaned deprecated glow_id that was converted in an earlier migration
    for force_name, forcedata in pairs(storage.forces or {}) do
      if forcedata.zone_assets then
        for zone_index, zone_assets in pairs(forcedata.zone_assets or {}) do
          if zone_assets.energy_beam_defence then
            for unit_number, defence in pairs(zone_assets.energy_beam_defence) do
              ---@class EnergyBeamDefenceInfo
              ---@field package glow_id any DEPRECATED
              if defence.glow_id then
                defence.glow_id = nil
              end
            end
          end
        end
      end
    end

    --remove orphaned deprecated beam_sprite_id that was converted in an earlier migration
    for _, tick_task in pairs(storage.tick_tasks or {}) do
      ---@cast tick_task SolarFlareTickTask
      for _, beam in pairs(tick_task.beams or {}) do
        if beam.beam_sprite_id then
          beam.beam_sprite_id = nil
        end
      end
    end

  end,

  ["0.7.34"] = function()
    for _, defences in pairs({storage.meteor_defences, storage.meteor_point_defences}) do
      for _, defence in pairs(defences) do
        if defence.charging_shape_id then
          defence.charging_shape = defence.charging_shape_id
          defence.charging_shape_id = nil
        end
        if defence.charging_text_id then
          defence.charging_text = defence.charging_text_id
          defence.charging_text_id = nil
        end
      end
    end

    local conquest_force = game.forces["conquest"]
    util.safe_research_tech(conquest_force, "repair-pack")

    -- Close spaceship gui for all players because we added another button
    -- They can open it again manually.
    for _, player in pairs(game.players) do
      SpaceshipGUI.gui_close(player)
    end
  end,

  ["0.7.35"] = function()
    local status_lookup = {
      [1] = "exterior",
      [2] = "wall_exterior",
      [3] = "bulkhead_exterior",
      [4] = "floor",
      [5] = "wall",
      [6] = "bulkhead",
      [7] = "floor_exterior",
      [8] = "floor_interior",
      [9] = "floor_console_disconnected",
      [10] = "wall_console_disconnected",
      [11] = "bulkhead_console_disconnected",
      [12] = "floor_console_connected",
      [13] = "wall_console_connected",
      [14] = "bulkhead_console_connected",
    }

    for _, force_data in pairs(storage.forces) do
      Spaceship.update_integrity_limit(game.forces[force_data.force_name])
    end

    for _, spaceships in pairs({
      storage.spaceships or {},
      storage.simulation_spaceships or { },
    }) do
      for _, spaceship in pairs(spaceships) do

        -- Update spaceship tile status
        for _, tiles_x in pairs(spaceship.check_tiles or { }) do
          for y, tile_status in pairs(tiles_x) do
            if type(tile_status) == "number" then
              local new_tile_status = status_lookup[tile_status]
              assert(new_tile_status, "Unknown tile status: " .. tile_status)
              tiles_x[y] = new_tile_status
            end
          end
        end
        for _, tiles_x in pairs(spaceship.known_tiles or { }) do
          for y, tile_status in pairs(tiles_x) do
            if type(tile_status) == "number" then
              local new_tile_status = status_lookup[tile_status]
              assert(new_tile_status, "Unknown tile status: " .. tile_status)
              tiles_x[y] = new_tile_status
            end
          end
        end

        -- Remove outdated fields
        spaceship.known_clamps = nil
        spaceship.check_clamps = nil

        -- Update surfce caching
        if spaceship.own_surface_index then
          spaceship.own_surface = game.get_surface(spaceship.own_surface_index)
          spaceship.on_own_surface = true
        end
        spaceship.own_surface_index = nil

        -- Remove deferred repath flag and force repath now
        if spaceship.scheduler.deferred_repath then
          SpaceshipScheduler.force_repath(spaceship)
          spaceship.scheduler.deferred_repath = nil
        end

      end
    end
  end,

  ["0.7.36"] = function()
    for _, force in pairs(game.forces) do
      if not SystemForces.is_system_force(force.name) then
        force.deconstruction_time_to_live = math.pow(2, 32) - 1
      end
    end

    -- This is the 0.7.0 migration, but for some reason it was only applied
    -- when storage.gate was present. So now we redo it with a check first
    -- that it hasn't been done already.
    for entity_number, shape_id in pairs(storage.beacon_overloaded_shapes) do
      if type(shape_id) == "number" then
        storage.beacon_overloaded_shapes[entity_number] = rendering.get_object_by_id(shape_id)
      end
    end
  end,

  ["0.7.37"] = function()
    -- Remove orphan spaceship console outputs
    for _, surface in pairs(game.surfaces) do
      for _, console in pairs(surface.find_entities_filtered{name = Spaceship.name_spaceship_console_output}) do
        if surface.find_entity(Spaceship.name_spaceship_console, console.position) then
          goto continue
        end

        if #surface.find_entities_filtered{
          name = "entity-ghost",
          ghost_name = Spaceship.name_spaceship_console,
          position = console.position} > 0
        then
          goto continue
        end

        console.destroy()
        ::continue::
      end
    end
  end,
}

Migrate.test_migrations = {
  --[[
  --Add migrations for testing in the following format with a custom named key.
  --When ready for release, change the name to the current version number and move to Migration.migrations above.
  ["My debug migrations"] = function()
    do_stuff()
  end,
  --]]
}

return Migrate
