local mod_gui = require("mod-gui")
local util = require("util")
local get_walkable_tile = util.get_walkable_tile
local balance = require("balance")
local config = require("config")
local production_score = require("production-score")
local kill_score = require("kill-score")
local insert = table.insert

local script_data =
{
  gui_actions = {},
  team_players = {},
  elements =
  {
    config = {},
    balance = {},
    import = {},
    admin = {},
    admin_button = {},
    spectate_button = {},
    join = {},
    progress_bar = {},
    team_frame = {},
    team_list_button = {},
    production_score_frame = {},
    production_score_inner_frame = {},
    recipe_frame = {},
    recipe_button = {},
    inventory = {},
    space_race_frame = {},
    kill_score_frame = {},
    oil_harvest_frame = {},
    team_tab = {},
    game_tab = {}
  },
  timers = {},
  setup_finished = false,
  ready_players = {},
  config = {},
  round_number = 0,
  selected_recipe = {},
  random = nil,
  team_names = {}
}

local statistics_period = 150 -- Seconds

local events =
{
  on_round_end = script.generate_event_name(),
  on_round_start = script.generate_event_name(),
  on_team_lost = script.generate_event_name(),
  on_team_won = script.generate_event_name(),
  on_player_joined_team = script.generate_event_name()
}

get_starting_area_radius = function(as_tiles)
  local surface = game.surfaces[1]
  local radius = math.ceil(surface.get_starting_area_radius() / 32)
  if as_tiles then
    return radius * 32
  end
  return radius
end

local lobby_name = "Lobby"
local get_lobby_surface = function()
  if game.surfaces[lobby_name] then return game.surfaces[lobby_name] end
  local surface = game.create_surface(lobby_name, {width = 1, height = 1})
  surface.set_tiles({{name = "out-of-map", position = {1,1}}})
  return surface
end

local is_in_map = function(width, height, position)
  return position.x >= -width
    and position.x < width
    and position.y >= -height
    and position.y < height
end

function create_spawn_positions()
  local settings = game.surfaces[1].map_gen_settings
  local width = settings.width
  local height = settings.height
  local height_scale = height / width
  local radius = get_starting_area_radius() + 1
  local diameter = radius * 32 * 2 * math.sqrt(2)
  local count = #script_data.config.teams
  if count == 1 then
    local positions =
    {
      {x = 0, y = 0}
    }
    script_data.spawn_offset = positions[1]
    script_data.spawn_positions = positions
    return positions
  end
  local angle = math.pi / count
  local hypotenuse = math.abs(math.sin(angle) * diameter)
  if hypotenuse < 0.01 then hypotenuse = diameter end
  --local min_distance = math.ceil((radius - 1) * 32) * (#script_data.config.teams)
  local min_distance = math.ceil(diameter * (diameter/hypotenuse))
  local displacement = script_data.config.team_config.average_team_displacement
  if displacement < min_distance then
    script_data.config.team_config.average_team_displacement = min_distance
    displacement = min_distance
  end

  local mag = displacement + (radius * 32)
  local y_scale = 1
  if mag > (height / 2) then
    --circle too tall
    y_scale = height_scale
  end
  local x_scale = 1
  if mag > (width / 2) then
    --circle too wide
    x_scale = 1 / height_scale
  end
  local random = script_data.random
  local horizontal_offset = (width > 10000 and (width / displacement) * 10) or 0
  local vertical_offset =  (height > 10000 and (height / displacement) * 10) or 0
  script_data.spawn_offset =
  {
    x = math.floor(0.5 + random(math.floor(-horizontal_offset), math.floor(horizontal_offset)) / 32) * 32,
    y = math.floor(0.5 + random(math.floor(-vertical_offset), math.floor(vertical_offset)) / 32) * 32
  }

  local distance = 0.5 * displacement
  local positions = {}

  local rotation_offset = (script_data.random() * (math.pi * 2))
  for k = 1, count do
    local rotation = rotation_offset + ((k * 2 * math.pi) / count)
    local X = (math.cos(rotation) * distance * x_scale) / 32
    if X > 0 then
      X = 32 * (math.floor(X))
    else
      X = 32 * (math.ceil(X))
    end
    local Y = (math.sin(rotation) * distance * y_scale) / 32
    if Y > 0 then
      Y = 32 * (math.floor(Y))
    else
      Y = 32 * (math.ceil(Y))
    end
    positions[k] = {x = X + script_data.spawn_offset.x, y = Y + script_data.spawn_offset.y}
  end

  script_data.spawn_positions = positions
  return positions
end

function create_next_surface()
  local name = "battle_surface_1"
  if game.surfaces[name] ~= nil then
    name = "battle_surface_2"
  end
  script_data.round_number = script_data.round_number + 1
  local settings = game.surfaces[1].map_gen_settings
  settings.starting_points = create_spawn_positions()
  settings.seed = script_data.config.game_config.seed
  script_data.surface = game.create_surface(name, settings)
  script_data.surface.always_day = script_data.config.team_config.always_day
end

function destroy_player_gui(player)
  local elements = script_data.elements
  local index = player.index
  for name, guis in pairs (elements) do
    local frame = guis[index]
    if frame and frame.valid then
      deregister_gui(frame)
      frame.destroy()
    end
    guis[index] = nil
  end

  for k, timer in pairs (script_data.timers) do
    if timer.valid and (timer.player_index == index) then
      timer.destroy()
    end
  end

end

function deregister_gui(gui)
  local player_gui_actions = script_data.gui_actions[gui.player_index]
  if not player_gui_actions then return end
  player_gui_actions[gui.index] = nil
  for k, child in pairs (gui.children) do
    deregister_gui(child)
  end
end

function register_gui_action(gui, param)
  local gui_actions = script_data.gui_actions
  local player_gui_actions = gui_actions[gui.player_index]
  if not player_gui_actions then
    gui_actions[gui.player_index] = {}
    player_gui_actions = gui_actions[gui.player_index]
  end
  player_gui_actions[gui.index] = param
end

local red = function(str)
  return "[color=1,0.2,0.2]"..str.."[/color]"
end

local green = function(str)
  return "[color=0.2,1,0.2]"..str.."[/color]"
end

function add_team_to_new_flow(team, flow, current_team, admin)
  local frame = flow.add{type = "frame", direction = "vertical", style = "bordered_frame"}
  frame.style.horizontally_stretchable = true

  local title_flow = frame.add{type = "flow", direction = "horizontal"}
  title_flow.style.vertical_align = "center"

  local show_flow = title_flow.add{type = "flow", style = "slot_table_spacing_horizontal_flow"}

  local label = show_flow.add{type = "label", caption = team.name, style = "caption_label"}
  label.style.font_color = get_color(team, true)

  add_pusher(show_flow)

  if admin then
    local edit_flow = title_flow.add{type = "flow", style = "slot_table_spacing_horizontal_flow", visible = false}
    edit_flow.style.horizontally_squashable = true
    local textfield = edit_flow.add{type = "textfield", text = team.name}
    textfield.style.width = 150
    local color_drop = edit_flow.add{type = "drop-down"}
    local index = 1
    for k, color in pairs (script_data.config.colors) do
      color_drop.add_item({"color."..color.name})
      if color.name == team.color then
        index = k
      end
    end
    color_drop.selected_index = index
    add_pusher(edit_flow)
    local textfield_confirm = edit_flow.add{type = "sprite-button", style = "tool_button_green", sprite = "utility/confirm_slot"}
    local textfield_cancel = edit_flow.add{type = "sprite-button", style = "tool_button_red", sprite = "utility/reset"}
    local edit_button = title_flow.add{type = "sprite-button", style = "tool_button", sprite = "utility/rename_icon_small_black"}
    local line = title_flow.add{type = "line", direction = "vertical"}
    line.style.height = 32
    line.style.vertically_stretchable = false
    local delete_button = title_flow.add{type = "sprite-button", style = "tool_button_red", sprite = "utility/trash"}
    register_gui_action(textfield_cancel, {type = "cancel_rename", edit_flow = edit_flow, show_flow = show_flow, buttons = {edit_button, delete_button}})
    register_gui_action(edit_button, {type = "rename_team", edit_flow = edit_flow, show_flow = show_flow, buttons = {edit_button, delete_button}})
    register_gui_action(delete_button, {type = "remove_team", team = team, frame = frame})
    register_gui_action(textfield_confirm, {type = "confirm_rename", textfield = textfield, team = team, dropdown = color_drop})
  end

  local line = frame.add{type = "line", direction = "horizontal"}

  local team_table = frame.add{type = "table", column_count = 2}

  team_table.add{type = "label", caption = {"", {"team"}, {"colon"}}, style = "description_label"}

  if admin then
    local drop_down = team_table.add{type = "drop-down"}
    local selected_index = 1
    drop_down.add_item({"no-team"})
    drop_down.add_item({"random-team"})
    for k = 1, #script_data.config.teams do
      drop_down.add_item(k)
      if k == team.team then
        selected_index = k + 2
      end
    end
    drop_down.selected_index = selected_index
    register_gui_action(drop_down, {type = "team_drop_down", team = team})
  else
    local caption
    if team.team == "-" then
      caption = {"no-team"}
    elseif team.team == "?" then
      caption = {"random-team"}
    else
      caption = team.team
    end
    team_table.add{type = "label", caption = caption}
  end

  local label = team_table.add{type = "label", caption = {"members"}, style = "description_label"}
  label.style.minimal_width = 150

  local ready = ""
  local first_ready = true
  local ready_data = script_data.ready_players

  local limit = script_data.config.team_config.max_players
  limit = (limit > 0 and limit) or math.huge
  local player_count = 0
  for k, member in pairs (team.members or {}) do
    player_count = player_count + 1
    if player_count > limit then
      team.members[k] = nil
      ready_data[k] = nil
      script_data.team_players[k] = nil
    else
      if first_ready then
        first_ready = false
      else
        ready = ready..", "
      end
      if ready_data[k] then
        ready = ready .. green(member.name)
      else
        ready = ready .. red(member.name)
      end
    end
  end
  if ready == "" then
    ready = {"none"}
  end
  local label = team_table.add{type = "label", caption = ready, style = "description_label"}
  label.style.single_line = false
  label.style.maximal_width = 400
  local within_limit = player_count < limit
  if within_limit and (not current_team or current_team ~= team) then
    local join_team = frame.add{type = "button", caption = {"join-team"}}
    join_team.style.font = "default"
    join_team.style.height = 24
    join_team.style.top_padding = 0
    join_team.style.bottom_padding = 0
    register_gui_action(join_team, {type = "join_team", team = team})
  end
  if current_team == team then
    local leave_team = frame.add{type = "button", caption = {"leave-team"}}
    leave_team.style.font = "default"
    leave_team.style.height = 24
    leave_team.style.top_padding = 0
    leave_team.style.bottom_padding = 0
    register_gui_action(leave_team, {type = "leave_team"})
  end

end

refresh_config = function(excluded_player_index)
  for k, player in pairs (game.connected_players) do
    if player.index ~= excluded_player_index then
      update_game_tab(player)
      update_team_tab(player)
      update_balance_tab(player)
      update_inventory_tab(player)
    end
  end
end

local name_allowed = function(name, team)
  if name == "" then return false end
  for k, other_team in pairs (script_data.config.teams) do
    if other_team.name == name then
      if other_team ~= team then
        return false
      end
    end
  end
  return true
end

local is_text_valid = function(text, strict)
  if text == "" then return false end
  local number = tonumber(text)
  if not number then return false end
  if number < 0 then return false end
  if number > 4294967295 then return false end
  if strict then return number >= 100 and number <= 254000 end
  return true
end

local check_all_ready = function()
  local all_ready = true
  for k, player in pairs (game.connected_players) do
    if not script_data.ready_players[player.index] then
      all_ready = false
      break
    end
  end
  if all_ready then
    start_all_ready_preparations()
  elseif script_data.ready_start_tick then
    script_data.ready_start_tick = nil
    game.print({"ready-cancelled"})
  end
end

update_inventory_tab = function(player)
  local group = script_data.elements.inventory[player.index]
  if not (group and group.valid) then return end
  group.clear()
  local admin = player.admin
  local config = script_data.config
  local types =
  {
    {name = {"equipment"}, list = config.equipment_list, option = config.starting_equipment},
    {name = {"chest"}, list = config.inventory_list, option = config.starting_chest}
  }
  for k, param in pairs (types) do
    local data = param.list
    local options = param.option
    if data and options then
      local inner = group.add{type = "frame", style = "bordered_frame", direction = "vertical"}
      inner.style.minimal_width = 500
      local top_flow = inner.add{type = "flow"}
      top_flow.add{type = "label", caption = param.name, style = "caption_label"}
      top_flow.style.vertical_align = "center"
      add_pusher(top_flow)
      local selected = options.selected
      if admin then
        local dropdown = top_flow.add{type = "drop-down"}
        dropdown.style.horizontally_stretchable = true
        register_gui_action(dropdown, {type = "starting_item_dropdown_changed", options = options})
        local index = 1
        for k, option in pairs (options.options) do
          if option == selected then index = k end
          dropdown.add_item({option})
        end
        dropdown.selected_index = index
      else
        top_flow.add{type = "label", caption = {selected}}
      end
      local scroll = inner.add{type = "scroll-pane"}
      local items = data[selected]
      if not items then return end
      if next(items) then
        local prototypes = game.item_prototypes
        local item_table = scroll.add{type = "table", column_count = 2, style = "bordered_table"}
        item_table.style.horizontally_stretchable = true
        for name, count in pairs (items) do
          local prototype = prototypes[name]
          if prototype then
            local flow = item_table.add{type = "flow"}
            flow.style.vertical_align = "center"
            if admin then
              local elem = flow.add{type = "choose-elem-button", elem_type = "item", style = "slot_button_in_shallow_frame"}
              elem.elem_value = name
              register_gui_action(elem, {type = "starting_item_elem_changed", items = items, previous = name})
            else
              local sprite = flow.add{type = "sprite", sprite = "item/"..name}
              sprite.style.width = 32
              sprite.style.height = 32
            end
            flow.add{type = "label", caption = prototype.localised_name}
            add_pusher(flow)
            if admin then
              local text = flow.add{type = "textfield", text = count, numeric = true, allow_decimal = false, allow_negative = false, style = "slider_value_textfield"}
              register_gui_action(text, {type = "starting_item_textfield_changed", items = items, key = name})
            else
              flow.add{type = "label", caption = count}
            end
          end
        end
      end

      if admin then
        local elem = scroll.add{type = "choose-elem-button", elem_type = "item", style = "slot_button_in_shallow_frame"}
        local pusher = scroll.add{type = "empty-widget"}
        pusher.style.vertically_stretchable = true
        register_gui_action(elem, {type = "starting_item_elem_changed", items = items})
      end
    end
  end

end

add_starting_chest_tab = function(tab_pane)
  local tab = tab_pane.add{type = "tab", caption = {"starting-items"}}
  local group = tab_pane.add{type = "flow"}
  tab_pane.add_tab(tab, group)
  local player = game.get_player(tab_pane.player_index)
  script_data.elements.inventory[player.index] = group
  update_inventory_tab(player)
end

local set_team = function(player_index, team)
  local current_team = script_data.team_players[player_index]
  if current_team then
    current_team.members[player_index] = nil
  end
  if team then
    team.members[player_index] = game.get_player(player_index)
    script_data.team_players[player_index] = team
  else
    script_data.team_players[player_index] = nil
  end
  script_data.ready_players[player_index] = nil
end

local gui_functions =
{
  new_team = function(event, param)
    local name
    repeat name = game.backer_names[math.random(#game.backer_names)]
    until name_allowed(name)
    local team =
    {
      name = name,
      color = script_data.config.colors[math.random(#script_data.config.colors)].name,
      members = {},
      team = "-"
    }
    insert(script_data.config.teams, team)
    refresh_config()
  end,
  remove_team = function(event, param)
    if #script_data.config.teams == 1 then
      game.players[event.player_index].print("Can't remove last team")
      return
    end
    for k, team in pairs (script_data.config.teams) do
      if team == param.team then
        table.remove(script_data.config.teams, k)
        for k, members in pairs (team.members) do
          script_data.ready_players[k] = nil
          script_data.team_players[k] = nil
        end
        break
      end
    end
    refresh_config()
  end,
  rename_team = function(event, param)
    param.edit_flow.visible = true
    param.show_flow.visible = false
    for k, button in pairs (param.buttons) do
      button.enabled = false
    end
  end,
  cancel_rename = function(event, param)
    param.edit_flow.visible = false
    param.show_flow.visible = true
    for k, button in pairs (param.buttons) do
      button.enabled = true
    end
  end,
  confirm_rename = function(event, param)
    local name = param.textfield.text
    if not name_allowed(name, param.team) then
      game.players[event.player_index].print("Name not allowed") --[[TODO locale]]
      return
    end
    param.team.name = name
    param.team.color = script_data.config.colors[param.dropdown.selected_index].name
    refresh_config()
  end,
  join_team = function(event, param)
    set_team(event.player_index, param.team)
    refresh_config()
    check_all_ready()
  end,
  leave_team = function(event, param)
    set_team(event.player_index)
    refresh_config()
    check_all_ready()
  end,
  team_drop_down = function(event, param)
    local gui = event.element
    if not (gui and gui.valid) then return end
    if event.name ~= defines.events.on_gui_selection_state_changed then return end
    local index
    if gui.selected_index == 1 then
      index = "-"
    elseif gui.selected_index == 2 then
      index = "?"
    else
      index = gui.selected_index - 2
    end
    param.team.team = index
    refresh_config(event.player_index)
  end,
  config_text_value_changed = function(event, param)
    if event.name ~= defines.events.on_gui_text_changed then return end
    local textfield = event.element
    if not (textfield and textfield.valid) then return end
    local text = textfield.text
    local valid = is_text_valid(text)
    if not valid then
      textfield.style = "invalid_value_textfield"
      textfield.style.horizontal_align = "center"
      return
    end
    textfield.style = "slider_value_textfield"
    param.config[param.key] = tonumber(text)
    refresh_config(event.player_index)
  end,
  config_dropdown_value_changed = function(event, param)
    if event.name ~= defines.events.on_gui_selection_state_changed then return end
    local dropdown = event.element
    if not (dropdown and dropdown.valid) then return end
    param.value.selected = param.value.options[dropdown.selected_index]
    refresh_config(event.player_index)
  end,
  config_boolean_changed = function(event, param)
    if event.name ~= defines.events.on_gui_checked_state_changed then return end
    local check = event.element
    if not (check and check.valid) then return end
    param.config[param.key] = check.state
    refresh_config(event.player_index)
  end,
  victory_config_boolean_changed = function(event, param)
    if event.name ~= defines.events.on_gui_checked_state_changed then return end
    local check = event.element
    if not (check and check.valid) then return end
    param.config[param.key].active = check.state
    refresh_config(event.player_index)
  end,
  start_round = function(event, param)
    start_round()
  end,
  ready_up = function(event, param)
    if event.name ~= defines.events.on_gui_checked_state_changed then return end
    local checkbox = event.element
    if not (checkbox and checkbox.valid) then return end
    local player = game.players[event.player_index]
    if checkbox.state then
      script_data.ready_players[event.player_index] = true
      game.print({"player-is-ready", player.name})
    else
      script_data.ready_players[event.player_index] = nil
      game.print({"player-is-not-ready", player.name})
    end
    refresh_config()
    check_all_ready()
  end,
  toggle_balance_options = function(event, param)
    toggle_balance_options_gui(game.players[event.player_index])
  end,
  reset_balance_options = function(event, param)
    for name, modifiers in pairs (script_data.config.modifier_list) do
      for key, value in pairs (modifiers) do
        modifiers[key] = 0
      end
    end
    refresh_config()
  end,
  balance_textfield_changed = function(event, param)
    if event.name ~= defines.events.on_gui_text_changed then return end
    local textfield = event.element
    if not (textfield and textfield.valid) then return end
    local text = textfield.text
    text = text:gsub("%%", "")
    local valid = is_text_valid(text, param.no_below_100)
    if not valid then
      textfield.style = "invalid_value_textfield"
      textfield.style.horizontal_align = "center"
      return
    end
    textfield.style = "slider_value_textfield"
    local value = (text - 100) / 100
    script_data.config.modifier_list[param.modifier][param.key] = value
    refresh_config(event.player_index)
  end,
  pvp_import = function(event, param)
    local player = game.players[event.player_index]
    if not (player and player.valid) then return end
    local gui = player.gui.screen
    local frame = gui.add{type = "frame", caption = {"gui-blueprint-library.import-string"}, direction = "vertical"}
    frame.auto_center = true

    local old = script_data.elements.import[player.index]
    if (old and old.valid) then old.destroy() end

    script_data.elements.import[player.index] = frame
    local textfield = frame.add{type = "text-box"}
    textfield.word_wrap = true
    textfield.style.height = player.display_resolution.height * 0.6 / player.display_scale
    textfield.style.width = player.display_resolution.width * 0.6 / player.display_scale
    local flow = frame.add{type = "flow", direction = "horizontal"}
    register_gui_action
    (
      flow.add{type = "button", caption = {"gui.close"}, style = "dialog_button"},
      {type = "import_export_close", frame = frame}
    )
    local pusher = flow.add{type = "empty-widget", style = "draggable_space"}
    pusher.style.horizontally_stretchable = true
    pusher.style.height = 32
    pusher.drag_target = frame
    local confirm_button = flow.add{type = "button", caption = {"gui-blueprint-library.import"}, style = "confirm_button"}
    confirm_button.style.minimal_width = 250
    register_gui_action
    (
      confirm_button,
      {type = "import_confirm", frame = frame, textfield = textfield}
    )
  end,
  import_confirm = function(event, param)
    local player = game.players[event.player_index]
    if not (player and player.valid) then return end
    local gui = player.gui.center
    local frame = param.frame
    if not (frame and frame.valid) then return end
    local textfield = param.textfield
    if not (textfield and textfield.valid) then return end
    local text = textfield.text
    if text == "" then player.print({"import-failed"}) return end
    local new_config = game.json_to_table(game.decode_string(text))
    if not new_config then
      player.print({"import-failed"})
      return
    end
    for k, v in pairs (new_config) do
      script_data.config[k] = v
    end

    local default_config = config.get_config()
    --We don't want to always append the new starting items to the default ones, so just clear them here.
    default_config.inventory_list = {}
    default_config.equipment_list = {}

    recursive_data_check(default_config, script_data.config)

    refresh_config()
    deregister_gui(frame)
    frame.destroy()
    script_data.elements.import[player.index] = nil
    player.print({"import-success"})
    log("Pvp config import success")
  end,
  pvp_export = function(event, param)
    local player = game.players[event.player_index]
    if not (player and player.valid) then return end
    local gui = player.gui.screen
    local frame = gui.add{type = "frame", caption = {"gui.export-to-string"}, direction = "vertical"}
    frame.auto_center = true

    local old = script_data.elements.import[player.index]
    if (old and old.valid) then old.destroy() end

    script_data.elements.import[player.index] = frame
    local textfield = frame.add{type = "text-box"}
    textfield.word_wrap = true
    textfield.read_only = true
    textfield.style.height = player.display_resolution.height * 0.6 / player.display_scale
    textfield.style.width = player.display_resolution.width * 0.6 / player.display_scale
    local config = script_data.config
    local data =
    {
      game_config = config.game_config,
      team_config = config.team_config,
      modifier_list = config.modifier_list,
      teams = config.teams,
      disabled_items = config.disabled_items,
      inventory_list = config.inventory_list,
      equipment_list = config.equipment_list,
      victory = config.victory,
      starting_equipment = config.starting_equipment,
      starting_chest = config.starting_chest
    }
    textfield.text = game.encode_string(game.table_to_json(data))
    local flow = frame.add{type = "flow"}
    register_gui_action
    (
      flow.add{type = "button", caption = {"gui.close"}, style = "dialog_button"},
      {type = "import_export_close", frame = frame}
    )
    local pusher = flow.add{type = "empty-widget", style = "draggable_space_with_no_right_margin"}
    pusher.style.horizontally_stretchable = true
    pusher.style.height = 32
    pusher.drag_target = frame
  end,
  import_export_close = function(event, param)
    local frame = param.frame
    if not (frame and frame.valid) then return end
    deregister_gui(frame)
    frame.destroy()
    script_data.elements.import[event.player_index] = nil
  end,
  starting_chest = function(event, param)
    toggle_starting_chest_gui(game.players[event.player_index])
  end,
  starting_item_textfield_changed = function(event, param)
    if event.name ~= defines.events.on_gui_text_changed then return end
    local textfield = event.element
    if not (textfield and textfield.valid) then return end
    local text = textfield.text
    local valid = is_text_valid(text)
    if not valid then
      textfield.style = "invalid_value_textfield"
      textfield.style.horizontal_align = "center"
      return
    end
    textfield.style = "slider_value_textfield"
    param.items[param.key] = tonumber(text)
    refresh_config(event.player_index)
  end,
  starting_item_elem_changed = function(event, param)
    if event.name ~= defines.events.on_gui_elem_changed then return end
    local element = event.element
    if not (element and element.valid) then return end
    local items = param.items
    local previous = param.previous
    if previous then
      items[param.previous] = nil
    end
    local name = element.elem_value
    if name then
      if items[name] then
        game.players[event.player_index].print("No doofus, its already there")
      else
        items[name] = game.item_prototypes[name].stack_size
      end
    end
    refresh_config()
  end,
  starting_item_dropdown_changed = function(event, param)
    if event.name ~= defines.events.on_gui_selection_state_changed then return end
    local dropdown = event.element
    if not (dropdown and dropdown.valid) then return end
    local data = param.options
    data.selected = data.options[dropdown.selected_index]
    refresh_config()
  end,
  disable_elem_changed = function(event, param)
    if event.name ~= defines.events.on_gui_elem_changed then return end

    local gui = event.element
    local player = game.players[event.player_index]
    if not (player and player.valid and gui and gui.valid) then return end
    local parent = gui.parent
    if not script_data.config.disabled_items then
      script_data.config.disabled_items = {}
    end
    local items = script_data.config.disabled_items
    local value = gui.elem_value
    if not value then
      local map = {}
      for k, child in pairs (parent.children) do
        if child.elem_value then
          map[child.elem_value] = true
        end
      end
      for item, bool in pairs (items) do
        if not map[item] then
          items[item] = nil
        end
      end
      deregister_gui(gui)
      gui.destroy()
      return
    end

    if items[value] then
      if items[value] ~= gui.index then
        gui.elem_value = nil
        player.print({"duplicate-disable"})
      end
    else
      items[value] = gui.index
      register_gui_action(parent.add{type = "choose-elem-button", elem_type = "item", style = "recipe_slot_button"}, {type = "disable_elem_changed"})
    end
    script_data.config.disabled_items = items
    refresh_config(event.player_index)
  end,
  join_spectator = function(event, param)
    local frame = param.frame
    if (frame and frame.valid) then
      deregister_gui(frame)
      frame.destroy()
    end
    spectator_join(game.players[event.player_index])
  end,
  join_random = function(event, param)
    local frame = param.frame
    if (frame and frame.valid) then
      deregister_gui(frame)
      frame.destroy()
    end
    local player = game.get_player(event.player_index)
    local teams = get_eligible_teams(player)
    if not teams then return end
    local team = teams[math.random(#teams)]

    set_player(player, team)

    for k, other_player in pairs (game.connected_players) do
      choose_joining_gui(other_player)
      choose_joining_gui(other_player)
      update_team_list_frame(other_player)
    end
  end,
  admin_button = function(event, param)
    local gui = event.element
    local player = game.players[event.player_index]
    local frame = script_data.elements.admin[event.player_index]
    if (frame and frame.valid) then
      frame.visible = not frame.visible
      return
    end
    local flow = mod_gui.get_frame_flow(player)
    local frame = flow.add{type = "frame", caption = {"admin"}, direction = "vertical"}
    script_data.elements.admin[player.index] = frame
    frame.visible = true
    register_gui_action(frame.add{type = "button", caption = {"end-round"}, tooltip = {"end-round-tooltip"}}, {type = "admin_end_round"})
    register_gui_action(frame.add{type = "button", caption = {"reroll-round"}, tooltip = {"reroll-round-tooltip"}}, {type = "admin_reroll_round"})
    register_gui_action(frame.add{type = "button", caption = {"restart-round"}, tooltip = {"restart-round-tooltip"}}, {type = "admin_restart_round"})
    register_gui_action(frame.add{type = "button", caption = {"admin-change-team"}, tooltip = {"admin-change-team-tooltip"}}, {type = "spectator_join_team_button"})
  end,
  admin_end_round = function(event, param)
    end_round(game.players[event.player_index])
  end,
  admin_reroll_round = function(event, param)
    game.print({"round-rerolled"})
    end_round()
    script_data.config.game_config.seed = math.random(2^32) - 1
    start_round()
    return
  end,
  admin_restart_round = function(event, param)
    game.print({"round-restarted"})
    end_round()
    start_round()
    return
  end,
  spectator_join_team_button = function(event, param)
    choose_joining_gui(game.players[event.player_index])
  end,
  pick_team = function(event, param)
    local gui = event.element
    local player = game.players[event.player_index]
    if not (gui and gui.valid and player and player.valid) then return end
    local team = param.team
    if not team then return end
    set_player(player, team)

    for k, other_player in pairs (game.connected_players) do
      choose_joining_gui(other_player)
      choose_joining_gui(other_player)
      update_team_list_frame(other_player)
    end

  end,
  list_teams_button = function(event, param)
    local player = game.players[event.player_index]
    if not (player and player.valid) then return end
    local frame = script_data.elements.team_frame[player.index]
    if frame and frame.valid then
      frame.destroy()
      script_data.elements.team_frame[player.index] = nil
      return
    end
    local flow = mod_gui.get_frame_flow(player)
    frame = flow.add{type = "frame", caption = {"teams"}, direction = "vertical"}
    frame.style.vertically_stretchable = false
    script_data.elements.team_frame[player.index] = frame
    update_team_list_frame(player)
  end,
  production_score_button = function(event, param)
    local gui = event.element
    local player = game.players[event.player_index]
    local frame = script_data.elements.production_score_frame[player.index]
    if frame and frame.valid then
      deregister_gui(frame)
      script_data.elements.production_score_frame[player.index] = nil
      frame.destroy()
      return
    end
    local flow = mod_gui.get_frame_flow(player)
    frame = flow.add{type = "frame", caption = {"production_score"}, direction = "vertical"}
    script_data.elements.production_score_frame[player.index] = frame
    frame.style.vertically_stretchable = false
    if script_data.config.victory.production_score.required_production_score > 0 then
      frame.add{type = "label", caption = {"", {"required_production_score"}, {"colon"}, " ", util.format_number(script_data.config.victory.production_score.required_production_score)}}
    end
    if script_data.config.game_config.time_limit > 0 then
      insert(script_data.timers, frame.add{type = "label", caption = {"time_left", get_time_left()}})
    end
    local inner_frame = frame.add{type = "frame", style = "inside_shallow_frame", direction = "vertical"}
    script_data.elements.production_score_inner_frame[player.index] = inner_frame
    inner_frame.style.left_padding = 8
    inner_frame.style.top_padding = 8
    inner_frame.style.right_padding = 8
    inner_frame.style.bottom_padding = 8
    local flow = frame.add{type = "flow", direction = "horizontal"}
    flow.add{type = "label", caption = {"", {"recipe-calculator"}, {"colon"}}}
    local recipe_button = flow.add{type = "choose-elem-button", elem_type = "recipe", style = "slot_button"}
    register_gui_action(recipe_button, {type = "recipe_picker_elem_changed", frame = frame})
    script_data.elements.recipe_button[player.index] = recipe_button
    flow.style.vertical_align = "center"
    update_production_score_frame(player)
    recipe_picker_elem_update(player)
  end,
  recipe_picker_elem_changed = function(event, param)
    if event.name ~= defines.events.on_gui_elem_changed then return end
    local elem_button = event.element
    if not (elem_button and elem_button.valid) then return end
    local player = game.players[event.player_index]
    if not (player and player.valid) then return end
    script_data.selected_recipe[player.index] = elem_button.elem_value
    recipe_picker_elem_update(player)
  end,
  calculator_button_press = function(event, param)
    on_calculator_button_press(event, param)
  end,
  space_race_button = function(event, param)
    local player = game.players[event.player_index]
    local frame = script_data.elements.space_race_frame[player.index]
    if frame and frame.valid then
      frame.destroy()
      script_data.elements.space_race_frame[player.index] = nil
      return
    end
    local flow = mod_gui.get_frame_flow(player)
    frame = flow.add{type = "frame", caption = {"space_race"}, direction = "vertical"}
    frame.style.vertically_stretchable = false
    script_data.elements.space_race_frame[player.index] = frame
    update_space_race_frame(player)
  end,
  kill_score_button = function(event, param)
    local player = game.players[event.player_index]
    local frame = script_data.elements.kill_score_frame[player.index]
    if frame and frame.valid then
      frame.destroy()
      script_data.elements.kill_score_frame[player.index] = nil
      return
    end
    local flow = mod_gui.get_frame_flow(player)
    frame = flow.add{type = "frame", caption = {"kill_score"}, direction = "vertical"}
    frame.style.vertically_stretchable = false
    script_data.elements.kill_score_frame[player.index] = frame
    update_kill_score_frame(player)
  end,
  oil_harvest_button = function(event, param)
    local player = game.players[event.player_index]
    local frame = script_data.elements.oil_harvest_frame[player.index]
    if (frame and frame.valid) then
      frame.destroy()
      script_data.elements.oil_harvest_frame[player.index] = nil
      return
    end
    local flow = mod_gui.get_frame_flow(player)
    frame = flow.add{type = "frame", caption = {"oil_harvest"}, direction = "vertical"}
    frame.style.vertically_stretchable = false
    script_data.elements.oil_harvest_frame[player.index] = frame
    update_oil_harvest_frame(player)
  end
}

function start_all_ready_preparations()
  local seconds = 10
  game.print({"everybody-ready", seconds})
  script_data.ready_start_tick = game.tick + (seconds * 60)
end

function add_new_config_gui(config_data, flow, admin)
  local bool_flow = flow.add{type = "flow", direction = "vertical"}
  bool_flow.style.horizontally_stretchable = true
  --local bottom_frame = flow.add{type = "frame", style = "bordered_frame_bottom"}
  local other_flow = flow.add{type = "table", column_count = 1, style = "bordered_table"}
  --other_flow.style.column_alignments[2] = "right"
  --other_flow.style.column_alignments[1] = "right"
  --other_flow.style.maximal_width = 350
  local items = game.item_prototypes
  for name, value in pairs (config_data) do
    if type(value) == "boolean" then
      local check = bool_flow.add{type = "checkbox", state = value, caption = config.localised_names[name] or {name}, ignored_by_interaction = not admin, tooltip = config.localised_tooltips[name] or {name.."_tooltip"}}
      register_gui_action(check, {type = "config_boolean_changed", config = config_data, key = name})
    end
    if tonumber(value) then
      local flow = other_flow.add{type = "table", column_count = 2}
      flow.style.column_alignments[2] = "right"
      local label = flow.add{type = "label", caption = config.localised_names[name] or {name}, tooltip = config.localised_tooltips[name] or {name.."_tooltip"}}
      label.style.horizontally_stretchable = true
      --local pusher = flow.add{type = "empty-widget"}
      --pusher.style.horizontally_stretchable = true
      if admin then
        text = flow.add{type = "textfield", text = value, numeric = true, allow_negative = false, allow_decimal = true, style = "slider_value_textfield"}
        text.style.maximal_width = 100
        register_gui_action(text, {type = "config_text_value_changed", config = config_data, key = name})
      else
        flow.add{type = "label", caption = value}
      end
    end
    if type(value) == "table" then
      local flow = other_flow.add{type = "table", column_count = 2}
      flow.style.column_alignments[2] = "right"

      local label = flow.add{type = "label", caption = config.localised_names[name] or {name}, tooltip = config.localised_tooltips[name] or {name.."_tooltip"}}
      label.style.horizontally_stretchable = true

      --local pusher = flow.add{type = "empty-widget"}
      --pusher.style.horizontally_stretchable = true
      if admin then
        local menu = flow.add{type = "drop-down", enabled = admin}
        register_gui_action(menu, {type = "config_dropdown_value_changed", value = value})
        local index
        for j, option in pairs (value.options) do
          if items[option] then
            menu.add_item(items[option].localised_name)
          else
            menu.add_item({option})
          end
          if option == value.selected then index = j end
        end
        menu.selected_index = index or 1
      else
        flow.add{type = "label", caption = (items[value.selected] and items[value.selected].localised_name) or {value.selected}}
      end
    end
  end
  local pusher = other_flow.add{type = "empty-widget"}
  pusher.style.vertically_stretchable = true
end

function add_victory_gui(config_data, flow, admin)
  local flow = flow.add{type = "table", column_count = 1, style = "bordered_table"}
  flow.style.width = 500
  flow.style.column_alignments[1] = "left"

  flow.add{type = "label", caption = {"victory-conditions"}, style = "caption_label"}
  for name, victory in pairs (config_data) do

    local inner_flow = flow.add{type = "flow"}
    inner_flow.style.height = 28
    inner_flow.style.vertical_align = "center"

    local check = inner_flow.add{type = "checkbox", state = victory.active, caption = config.localised_names[name] or {name}, ignored_by_interaction = not admin, tooltip = config.localised_tooltips[name] or {name.."_tooltip"}}
    check.style.width = 150
    check.style.vertical_align = "center"
    register_gui_action(check, {type = "victory_config_boolean_changed", config = config_data, key = name})

    for extra_name, extra in pairs (victory) do
      if extra_name ~= "active" then

        add_pusher(inner_flow)
        local line = inner_flow.add{type = "line", direction = "vertical"}
        line.style.vertically_stretchable = true

        local label = inner_flow.add{type = "label", caption = config.localised_names[extra_name] or {extra_name}, tooltip = config.localised_tooltips[extra_name] or {extra_name.."_tooltip"}}
        label.style.width = 180

        if admin then
          text = inner_flow.add{type = "textfield", text = extra, numeric = true, allow_negative = false, allow_decimal = false, style = "slider_value_textfield"}
          register_gui_action(text, {type = "config_text_value_changed", config = victory, key = extra_name})
        else
          inner_flow.add{type = "label", caption = extra}
        end

      end
    end

  end
end

function add_team_tab(tab_pane)
  local tab = tab_pane.add{type = "tab", caption = {"team-settings"}}
  local group = tab_pane.add{type = "flow"}
  tab_pane.add_tab(tab, group)
  local player = game.get_player(tab_pane.player_index)
  script_data.elements.team_tab[player.index] = group
  update_team_tab(player)
end

function update_team_tab(player)
  local admin = player.admin
  local holding_table_1 = script_data.elements.team_tab[player.index]
  if not (holding_table_1 and holding_table_1.valid) then return end
  holding_table_1.clear()

  local team_lobby = holding_table_1.add{type = "flow", direction = "vertical"}
  team_lobby.style.vertically_stretchable = true

  local title_flow = team_lobby.add{type = "frame", style = "bordered_frame"}
  title_flow.style.vertical_align = "center"
  local label = title_flow.add{type = "label", caption = {"teams"}, style = "caption_label"}
  label.style.height = 28
  label.style.vertical_align = "center"

  if admin then
    add_pusher(title_flow)
    local button = title_flow.add{type = "button", caption = {"add-team"}, tooltip = {"add-team-tooltip"}, enabled = #script_data.config.teams < 24}
    register_gui_action(button, {type = "new_team", frame = flow})
  end

  local scroll = team_lobby.add{type = "scroll-pane", direction = "vertical"}
  local current_team = script_data.team_players[player.index]

  for k, team in pairs (script_data.config.teams) do
    add_team_to_new_flow(team, scroll, current_team, admin)
  end

  local ready_data = script_data.ready_players
  local str = ""
  local first = true
  for k, player in pairs (game.connected_players) do
    if not script_data.team_players[player.index] then
      if first then
        first = false
      else
        str = str.. ", "
      end
      if ready_data[player.index] then
        str = str .. green(player.name)
      else
        str = str .. red(player.name)
      end
    end
  end

  if first then str = {"none"} end

  local bottom_frame = team_lobby.add{type = "frame", style = "bordered_frame"}
  bottom_frame.add{type = "label", caption = {"unassigned-players",  str}}
  bottom_frame.style.horizontally_stretchable = true

  local team_settings = holding_table_1.add{type = "frame", direction = "vertical", style = "bordered_frame"}
  team_settings.add{type = "label", caption = {"team-settings"}, style = "caption_label"}
  team_settings.style.vertically_stretchable = true
  team_settings.style.horizontally_stretchable = true
  local line = team_settings.add{type = "line", direction = "horizontal"}
  line.style.horizontally_stretchable = true

  add_new_config_gui(script_data.config.team_config, team_settings, admin)

end

function add_game_tab(tab_pane)
  local tab = tab_pane.add{type = "tab", caption = {"game-settings"}}
  local group = tab_pane.add{type = "flow"}
  tab_pane.add_tab(tab, group)
  local player = game.get_player(tab_pane.player_index)
  script_data.elements.game_tab[player.index] = group
  update_game_tab(player)
end

function update_game_tab(player)
  local admin = player.admin
  local holding_table_2 = script_data.elements.game_tab[player.index]
  if not (holding_table_2 and holding_table_2.valid) then return end
  holding_table_2.clear()
  local game_settings = holding_table_2.add{type = "table", column_count = 1, style = "bordered_table"}
  game_settings.add{type = "label", caption = {"game-settings"}, style = "caption_label"}
  game_settings.style.vertically_stretchable = true
  game_settings.style.horizontally_stretchable = true
  local inner_table = game_settings.add{type = "flow", column_count = 2}

  add_new_config_gui(script_data.config.game_config, inner_table, admin)

  local other_flow = holding_table_2.add{type = "flow", direction = "vertical"}
  other_flow.style.vertically_stretchable = true
  other_flow.style.horizontally_stretchable = false

  local victory = other_flow.add{type = "flow", direction = "vertical"}
  --victory.style.vertically_stretchable = true
  add_victory_gui(script_data.config.victory, victory, admin)
  local disable_items = other_flow.add{type = "flow"}
  disable_items.style.vertically_stretchable = true
  disable_items.style.horizontally_stretchable = true
  create_disable_frame(disable_items)

end

function create_config_gui(player)
  if not (player and player.valid and player.connected) then return end
  local old = script_data.elements.config[player.index]
  if (old and old.valid) then
    deregister_gui(old)
    old.destroy()
  end
  local admin = player.admin
  local gui = player.gui.screen
  local upper_frame = gui.add{type = "frame", caption = {"pvp-configuration"}, direction = "vertical"}
  --upper_frame.style.minimal_width = player.display_resolution.width * 0.75 / player.display_scale
  script_data.elements.config[player.index] = upper_frame
  upper_frame.style.vertically_stretchable = false
  local deep = upper_frame.add{type = "frame", style = "inside_deep_frame", direction = "vertical"}
  local tab_pane = deep.add{type = "tabbed-pane"}
  tab_pane.style.horizontally_stretchable = true
  tab_pane.style.top_padding = 2
  tab_pane.selected_tab_index = 1
  tab_pane.style.maximal_height = 1080 * 0.8
  add_team_tab(tab_pane)
  add_game_tab(tab_pane)
  add_balance_tab(tab_pane)
  add_starting_chest_tab(tab_pane)

  local footer = deep.add{type = "frame", style = "subfooter_frame"}
  footer.style.horizontally_stretchable = true
  register_gui_action(footer.add{type = "sprite-button", sprite = "utility/export", tooltip = {"gui.export-to-string"}, style = "tool_button"}, {type = "pvp_export"})
  if admin then
    register_gui_action(footer.add{type = "sprite-button", sprite = "utility/import", tooltip = {"gui-blueprint-library.import-string"}, style = "tool_button"}, {type = "pvp_import"})
  end

  local button_flow = upper_frame.add{type = "flow"}
  button_flow.style.vertical_align = "center"

  local pusher = button_flow.add{type = "empty-widget", style = "draggable_space_with_no_left_margin"}
  pusher.style.horizontally_stretchable = true
  pusher.style.height = 24
  pusher.drag_target = upper_frame
  local ready = script_data.ready_players[player.index] or false
  local ready_up = button_flow.add{type = "checkbox", caption = {"ready"}, state = ready}
  ready_up.style.right_padding = 8

  register_gui_action(ready_up, {type = "ready_up"})
  local start_button = button_flow.add{type = "button", style = "confirm_button", caption = {"start-round"}, enabled = admin}
  start_button.style.minimal_width = 250
  register_gui_action(start_button, {type = "start_round"})
  upper_frame.auto_center = true
end

function end_round(admin)
  destroy_config_for_all()
  for k, player in pairs (game.players) do
    player.force = game.forces.player
    player.tag = ""
    destroy_player_gui(player)
    if player.connected then
      if player.ticks_to_respawn then
        player.ticks_to_respawn = nil
      end
      local character = player.character
      player.character = nil
      if character then character.destroy() end
      player.set_controller{type = defines.controllers.spectator}
      player.teleport({0,1000}, get_lobby_surface())
      create_config_gui(player)
    end
  end
  if script_data.surface and script_data.surface.valid then
    game.delete_surface(script_data.surface)
  end
  if admin then
    game.print({"admin-ended-round", admin.name})
  end
  script_data.setup_finished = false
  script_data.check_starting_area_generation = false
  script_data.average_score = nil
  script_data.scores = nil
  script_data.exclusion_map = nil
  script_data.protected_teams = nil
  script_data.check_base_exclusion = nil
  script_data.oil_harvest_scores = nil
  script_data.production_scores = nil
  script_data.rocket_scores = nil
  script_data.kill_scores = nil
  script_data.last_defcon_tick = nil
  script_data.next_defcon_tech = nil
  script_data.silos = nil
  script.raise_event(events.on_round_end, {})
end

game_mode_buttons = function() return
  {
    ["production_score"] = {type = "button", caption = {"production_score"}, action = "production_score_button", style = mod_gui.button_style},
    ["oil_harvest"] = {type = "button", caption = {"oil_harvest"}, action = "oil_harvest_button", style = mod_gui.button_style},
    ["kill_score"] = {type = "button", caption = {"kill_score"}, action = "kill_score_button", style = mod_gui.button_style},
    ["space_race"] = {type = "button", caption = {"space_race"}, action = "space_race_button", style = mod_gui.button_style}
  }
end

function init_player_gui(player)
  destroy_player_gui(player)

  if script_data.progress then
    update_progress_bar()
    return
  end

  if script_data.setup_finished == false then
    create_config_gui(player)
    return
  end

  if player.force.name == "player" then
    choose_joining_gui(player)
    return
  end

  local button_flow = mod_gui.get_button_flow(player)

  local list_teams_button = button_flow.add{type = "button", caption = {"teams"}, style = mod_gui.button_style}
  register_gui_action(list_teams_button, {type = "list_teams_button"})
  script_data.elements.team_list_button[player.index] = list_teams_button

  for name, button in pairs (game_mode_buttons()) do
    if not script_data.elements[name] then
      script_data.elements[name] = {}
    end
    if script_data.config.victory[name].active then
      local element = button_flow.add(button)
      register_gui_action(element, {type = button.action})
      script_data.elements[name][player.index] = element
    end
  end

  if player.admin then
    local admin_button = button_flow.add{type = "button", caption = {"admin"}, style = mod_gui.button_style}
    register_gui_action(admin_button, {type = "admin_button"})
    script_data.elements.admin_button[player.index] = admin_button
  end

  if player.force.name == "neutral" then
    local spectate_button = button_flow.add{type = "button", caption = {"join-team"}, style = mod_gui.button_style}
    register_gui_action(spectate_button, {type = "spectator_join_team_button"})
    script_data.elements.spectate_button[player.index] = spectate_button
  end

end

function get_color(team, lighten)
  local index = script_data.config.color_map[team.color]
  if not index then
    --Unknown color
    team.color = script_data.config.colors[math.random(#script_data.config.colors)].name
    index = script_data.config.color_map[team.color]
  end
  local c = script_data.config.colors[index].color
  if lighten then
    return {r = 1 - (1 - c.r) * 0.5, g = 1 - (1 - c.g) * 0.5, b = 1 - (1 - c.b) * 0.5, a = 1}
  end
  return c
end

function add_player_list_gui(force, gui)
  if not (force and force.valid) then return end
  if #force.players == 0 then
    gui.add{type = "label", caption = {"none"}}
    return
  end
  local scroll = gui.add{type = "scroll-pane"}
  scroll.style.maximal_height = 120
  local name_table = scroll.add{type = "table", column_count = 1}
  name_table.style.vertical_spacing = 0
  local added = {}
  local first = true
  if #force.connected_players > 0 then
    local online_names = ""
    for k, player in pairs (force.connected_players) do
      if not first then
        online_names = online_names..", "
      end
      first = false
      online_names = online_names..player.name
      added[player.name] = true
    end
    local online_label = name_table.add{type = "label", caption = {"online", online_names}}
    online_label.style.single_line = false
    online_label.style.maximal_width = 180
  end
  first = true
  if #force.players > #force.connected_players then
    local offline_names = ""
    for k, player in pairs (force.players) do
      if not added[player.name] then
      if not first then
        offline_names = offline_names..", "
      end
      first = false
      offline_names = offline_names..player.name
      added[player.name] = true
      end
    end
    local offline_label = name_table.add{type = "label", caption = {"offline", offline_names}}
    offline_label.style.single_line = false
    offline_label.style.font_color = {r = 0.7, g = 0.7, b = 0.7}
    offline_label.style.maximal_width = 180
  end
end

function set_player(player, team, mute)
  local force = game.forces[team.name]
  local old_force = player.force
  local surface = script_data.surface
  if not surface.valid then return end
  local position = surface.find_non_colliding_position("character", force.get_spawn_position(surface), get_starting_area_radius(true), 2)
  if not position then
    player.print({"cant-find-position"})
    choose_joining_gui(player)
    return
  end
  local character = player.surface == surface and player.character
  if character then
    character.teleport(position)
  else
    character = surface.create_entity{name = "character", position = position, force = force}
  end
  player.force = force
  player.teleport(position, surface)

  player.character = nil
  player.set_controller
  {
    type = defines.controllers.character,
    character = character
  }

  player.color = get_color(team)
  player.chat_color = get_color(team, true)
  player.tag = "["..force.name.."]"

  init_player_gui(player)
  set_team(player.index, team)

  for k, other_player in pairs (game.connected_players) do
    choose_joining_gui(other_player)
    choose_joining_gui(other_player)
    update_team_list_frame(other_player)
  end

  local artillery_remote = script_data.config.prototypes.artillery_remote
  if script_data.config.game_config.team_artillery and script_data.config.game_config.give_artillery_remote and game.item_prototypes[artillery_remote] then
    player.insert(artillery_remote)
  end
  config.give_equipment(player)
  balance.apply_character_modifiers(player)

  if not mute then
    game.print({"joined", player.name, player.force.name})
  end

  check_force_protection(force)
  check_force_protection(old_force)
  script.raise_event(events.on_player_joined_team, {player_index = player.index, team = team, force = force})
end

function choose_joining_gui(player)
  local frame = script_data.elements.join[player.index]
  if (frame and frame.valid) then
    deregister_gui(frame)
    frame.destroy()
    return
  end
  local teams = get_eligible_teams(player)
  if not teams then return end
  local gui = player.gui.screen
  local frame = gui.add{type = "frame", direction = "vertical"}

  local title_flow = frame.add{type = "flow", direction = "horizontal"}
  title_flow.style.horizontally_stretchable = true

  local title_label = title_flow.add{type = "label", caption = {"pick-join"}, style = "heading_1_label"}
  title_label.drag_target = frame

  local title_pusher = title_flow.add{type = "empty-widget", style = "draggable_space_header"}
  title_pusher.style.vertically_stretchable = true
  title_pusher.style.horizontally_stretchable = true
  title_pusher.drag_target = frame

  --If they are on player force, it means they aren't on a proper team already, don't let them close the choose team frame.
  if player.force.name ~= "player" then
    local title_close_button = title_flow.add{type = "sprite-button", style = "frame_action_button", sprite = "utility/close_white"}
    register_gui_action(title_close_button, {type = "spectator_join_team_button"})
  end

  script_data.elements.join[player.index] = frame
  local inner_frame = frame.add{type = "frame", style = "inside_shallow_frame", direction = "vertical"}
  inner_frame.style.left_padding = 8
  inner_frame.style.top_padding = 8
  inner_frame.style.right_padding = 8
  inner_frame.style.bottom_padding = 8
  local pick_join_table = inner_frame.add{type = "table", column_count = 4}
  pick_join_table.style.horizontal_spacing = 16
  pick_join_table.style.vertical_spacing = 8
  pick_join_table.draw_horizontal_lines = true
  pick_join_table.draw_vertical_lines = true
  pick_join_table.style.column_alignments[3] = "right"
  pick_join_table.add{type = "label", caption = {"team-name"}}.style.font = "default-semibold"
  pick_join_table.add{type = "label", caption = {"players"}}.style.font = "default-semibold"
  pick_join_table.add{type = "label", caption = {"team-number"}}.style.font = "default-semibold"
  pick_join_table.add{type = "label"}
  for k, team in pairs (teams) do
    local force = game.forces[team.name]
    if force then
      local name = pick_join_table.add{type = "label", caption = force.name}
      name.style.font = "default-semibold"
      name.style.font_color = get_color(team, true)
      add_player_list_gui(force, pick_join_table)
      local caption
      if tonumber(team.team) then
        caption = team.team
      elseif team.team:find("?") then
        caption = team.team:gsub("?", "")
      else
        caption = team.team
      end
      pick_join_table.add{type = "label", caption = caption}
      local join_button = pick_join_table.add{type = "button", caption = {"join"}}
      register_gui_action(join_button, {type = "pick_team", team = team})
    end
  end
  local button_flow = frame.add{type = "flow", direction = "horizontal"}
  register_gui_action(button_flow.add{type = "button", caption = {"join-spectator"}}, {type = "join_spectator", frame = frame})
  register_gui_action(button_flow.add{type = "button", caption = {"join-random"}}, {type = "join_random", frame = frame})
  local drag = button_flow.add{type = "empty-widget", style = "draggable_space_with_no_right_margin"}
  drag.style.horizontally_stretchable = true
  drag.style.height = 28
  drag.drag_target = frame
  frame.auto_center = true
end

function update_balance_tab(player)
  local inner = script_data.elements.balance[player.index]
  if not (inner and inner.valid) then return end
  inner.clear()
  local scrollpane = inner.add{type = "scroll-pane"}
  local big_table = scrollpane.add{type = "table", column_count = 5, direction = "horizontal"}
  big_table.style.horizontally_stretchable = true
  --big_table.style.horizontal_spacing = 32
  --big_table.draw_vertical_lines = true
  local entities = game.entity_prototypes
  local ammos = game.ammo_category_prototypes
  local admin = player.admin
  local modifier_list = script_data.config.modifier_list
  if not modifier_list then
    balance.init()
    modifier_list = script_data.config.modifier_list
  end
  for modifier_name, array in pairs (modifier_list) do
    local flow = big_table.add{type = "table", style = "bordered_table", column_count = 1}
    flow.style.vertically_stretchable = true
    flow.style.horizontally_stretchable = true
    flow.add{type = "label", style = "caption_label", caption = {modifier_name}}
    local inner = flow.add{type = "flow", direction = "vertical"}
    inner.style.vertically_stretchable = true
    local table = inner.add{type = "table", column_count = 2}
    table.style.column_alignments[1] = "left"
    table.style.column_alignments[2] = "right"
    for name, modifier in pairs (array) do
      if modifier_name == "ammo_damage_modifier" then
        local string = "ammo-category-name."..name
        table.add{type = "label", caption = ammos[name].localised_name}
      elseif modifier_name == "gun_speed_modifier" then
        table.add{type = "label", caption = ammos[name].localised_name}
      elseif modifier_name == "turret_attack_modifier" then
        table.add{type = "label", caption = entities[name].localised_name}
      elseif modifier_name == "character_modifiers" then
        table.add{type = "label", caption = {name}}
      elseif modifier_name == "force_modifiers" then
        table.add{type = "label", caption = {name}}
      end
      if admin then
        local input = table.add{type = "textfield", numeric = true, allow_decimal = true, allow_negative = false, style = "slider_value_textfield"}
        register_gui_action(input, {type = "balance_textfield_changed", modifier = modifier_name, key = name, no_below_100 = (modifier_name == "force_modifiers")})
        input.text = tostring((modifier * 100) + 100).."%"
        input.style.maximal_width = 60
      else
        table.add{type = "label", caption = tostring((modifier * 100) + 100).."%"}
      end
    end
  end
end

function add_balance_tab(tab_pane)
  local tab = tab_pane.add{type = "tab", caption = {"balance-options"}}
  local inner = tab_pane.add{type = "flow"}
  inner.style.horizontally_stretchable = true
  tab_pane.add_tab(tab, inner)
  local player = game.get_player(tab_pane.player_index)
  script_data.elements.balance[player.index] = inner
  update_balance_tab(player)
end

function create_disable_frame(gui)
  local inner = gui.add{type = "table", style = "bordered_table", column_count = 1}
  inner.add{type = "label", caption = {"disabled-items"}, style = "caption_label"}
  local frame = inner.add{type = "frame", style = "filter_scroll_pane_background_frame"}
  frame.style.width = 12 * 40
  local disable_table = frame.add{type = "table", column_count = 12, style = "filter_slot_table"}
  local items = game.item_prototypes
  local player = game.players[gui.player_index]
  local admin = player.admin
  if script_data.config.disabled_items then
    for item, bool in pairs (script_data.config.disabled_items) do
      local prototype = items[item]
      if prototype then
        if admin then
          local choose = disable_table.add{type = "choose-elem-button", elem_type = "item", style = "recipe_slot_button"}
          choose.elem_value = item
          register_gui_action(choose, {type = "disable_elem_changed"})
        else
          local icon = disable_table.add{type = "sprite", sprite = "item/"..item, tooltip = prototype.localised_name}
          icon.style.width = 32
          icon.style.height = 32
        end
      end
    end
  end
  if admin then
    local choose = disable_table.add{type = "choose-elem-button", elem_type = "item", style = "recipe_slot_button"}
    register_gui_action(choose, {type = "disable_elem_changed"})
  end
end

function start_round()

  game.reset_time_played()

  destroy_config_for_all()

  script_data.random = game.create_random_generator(script_data.config.game_config.seed)
  script_data.ready_start_tick = nil
  script_data.setup_finished = false
  script_data.team_won = false

  create_next_surface()
  setup_teams()
  chart_starting_area_for_force_spawns()
  set_evolution_factor()
  set_difficulty()

end

function get_eligible_teams(player)
  local limit = script_data.config.team_config.max_players
  local teams = {}
  for k, team in pairs (script_data.config.teams) do
    local force = game.forces[team.name]
    if force then
      if limit <= 0 or #force.connected_players < limit or player.admin then
        insert(teams, team)
      end
    end
  end
  if #teams == 0 then
    spectator_join(player)
    player.print({"no-space-available"})
    return
  end
  return teams
end

function destroy_config_for_all()

  for name, frames in pairs (script_data.elements) do
    for k, frame in pairs (frames) do
      if (frame and frame.valid) then
        deregister_gui(frame)
        frame.destroy()
      end
    end
    script_data.elements[name] = {}
  end

  script_data.ready_players = {}
end

function set_evolution_factor()
  local n = script_data.config.team_config.evolution_factor
  if n >= 1 then
    n = 1
  end
  if n <= 0 then
    n = 0
  end
  for k, force in pairs (game.forces) do
    force.evolution_factor = n
  end
  script_data.config.team_config.evolution_factor = n
end

function set_difficulty()
  game.difficulty_settings.technology_price_multiplier = script_data.config.team_config.technology_price_multiplier or 1
end

function spectator_join(player)
  local character = player.character
  player.set_controller{type = defines.controllers.spectator}
  if character then character.die() end
  player.force = "neutral"
  player.teleport(script_data.spawn_offset, script_data.surface)
  player.tag = ""
  player.chat_color = {r = 1, g = 1, b = 1, a = 1}
  init_player_gui(player)
  game.print({"joined-spectator", player.name})
  set_team(player.index)
end

function update_team_list_frame(player)
  if not (player and player.valid) then return end
  local frame = script_data.elements.team_frame[player.index]
  if not (frame and frame.valid) then return end
  frame.clear()
  local inner = frame.add{type = "frame", style = "inside_shallow_frame"}
  inner.style.left_padding = 8
  inner.style.right_padding = 8
  inner.style.top_padding = 8
  inner.style.bottom_padding = 8
  local scroll = inner.add{type = "scroll-pane"}
  local team_table = scroll.add{type = "table", column_count = 2}
  team_table.style.vertical_spacing = 8
  team_table.style.horizontal_spacing = 16
  team_table.draw_horizontal_lines = true
  team_table.draw_vertical_lines = true
  team_table.add{type = "label", caption = {"team-name"}, style = "bold_label"}
  team_table.add{type = "label", caption = {"players"}, style = "bold_label"}
  for k, team in pairs (script_data.config.teams) do
    local force = game.forces[team.name]
    if force then
      local label = team_table.add{type = "label", caption = team.name, style = "description_label"}
      label.style.font_color = get_color(team, true)
      add_player_list_gui(force, team_table)
    end
  end
end

function format_time(ticks)
  local hours = math.floor(ticks / (60 * 60 * 60))
  ticks = ticks - hours * (60 * 60 * 60)
  local minutes = math.floor(ticks / (60 * 60))
  ticks = ticks - minutes * (60 * 60)
  local seconds = math.floor(ticks / 60)
  if hours > 0 then
    return string.format("%d:%02d:%02d", hours, minutes, seconds)
  else
    return string.format("%d:%02d", minutes, seconds)
  end
end

function get_time_left()
  if not script_data.round_start_tick then return "Invalid" end
  if not script_data.config.game_config.time_limit then return "Invalid" end
  return format_time((math.max(script_data.round_start_tick + (script_data.config.game_config.time_limit * 60 * 60) - game.tick, 0)))
end

function update_production_score_frame(player)
  local frame = script_data.elements.production_score_inner_frame[player.index]
  if not (frame and frame.valid) then return end
  frame.clear()
  local information_table = frame.add{type = "table", column_count = 4}
  information_table.draw_horizontal_line_after_headers = true
  information_table.draw_vertical_lines = true
  information_table.style.horizontal_spacing = 16
  information_table.style.vertical_spacing = 8
  information_table.style.column_alignments[3] = "right"
  information_table.style.column_alignments[4] = "right"

  for k, caption in pairs ({"", "team-name", "score", "score_per_minute"}) do
    local label = information_table.add{type = "label", caption = {caption}, tooltip = {caption.."_tooltip"}}
    label.style.font = "default-bold"
  end
  local team_map = {}
  for k, team in pairs (script_data.config.teams) do
    team_map[team.name] = team
  end
  local average_score = script_data.average_score
  if not average_score then return end
  local rank = 1
  for name, score in spairs (script_data.production_scores, function(t, a, b) return t[b] < t[a] end) do
    if not average_score[name] then
      average_score = nil
      return
    end
    if team_map[name] then
      local position = information_table.add{type = "label", caption = "#"..rank}
      if name == player.force.name then
        position.style.font = "default-semibold"
        position.style.font_color = {r = 1, g = 1}
      end
      local label = information_table.add{type = "label", caption = name}
      label.style.font = "default-semibold"
      label.style.font_color = get_color(team_map[name], true)
      information_table.add{type = "label", caption = util.format_number(score)}
      local delta_score = (score - (average_score[name] / statistics_period)) * (60 / statistics_period) * 2
      local delta_label = information_table.add{type = "label", caption = util.format_number(math.floor(delta_score))}
      if delta_score < 0 then
        delta_label.style.font_color = {r = 1, g = 0.2, b = 0.2}
      end
      rank = rank + 1
    end
  end
end

function update_oil_harvest_frame(player)

  local frame = script_data.elements.oil_harvest_frame[player.index]
  if not (frame and frame.valid) then
    return
  end
  frame.clear()

  if script_data.config.victory.oil_harvest.required_oil > 0 then
    frame.add{type = "label", caption = {"", {"required_oil"}, {"colon"}, " ", util.format_number(script_data.config.victory.oil_harvest.required_oil)}}
  end

  if script_data.config.game_config.time_limit > 0 then
    frame.add{type = "label", caption = {"time_left", get_time_left()}}
  end

  local inner_frame = frame.add{type = "frame", style = "inside_shallow_frame", direction = "vertical"}
  inner_frame.style.left_padding = 8
  inner_frame.style.top_padding = 8
  inner_frame.style.right_padding = 8
  inner_frame.style.bottom_padding = 8
  local information_table = inner_frame.add{type = "table", column_count = 3}
  information_table.draw_horizontal_line_after_headers = true
  information_table.draw_vertical_lines = true
  information_table.style.horizontal_spacing = 16
  information_table.style.vertical_spacing = 8
  information_table.style.column_alignments[3] = "right"

  for k, caption in pairs ({"", "team-name", "oil_harvest"}) do
    local label = information_table.add{type = "label", caption = {caption}}
    label.style.font = "default-bold"
  end
  local team_map = {}
  for k, team in pairs (script_data.config.teams) do
    team_map[team.name] = team
  end
  if not script_data.oil_harvest_scores then
    script_data.oil_harvest_scores = {}
  end
  local rank = 1
  for name, score in spairs (script_data.oil_harvest_scores, function(t, a, b) return t[b] < t[a] end) do
    if team_map[name] then
      local position = information_table.add{type = "label", caption = "#"..rank}
      if name == player.force.name then
        position.style.font = "default-semibold"
        position.style.font_color = {r = 1, g = 1}
      end
      local label = information_table.add{type = "label", caption = name}
      label.style.font = "default-semibold"
      label.style.font_color = get_color(team_map[name], true)
      information_table.add{type = "label", caption = util.format_number(math.floor(score))}
      rank = rank + 1
    end
  end
end

function update_kill_score_frame(player)

  local frame = script_data.elements.kill_score_frame[player.index]
  if not (frame and frame.valid) then
    return
  end
  frame.clear()

  if script_data.config.victory.kill_score.required_kill_score > 0 then
    frame.add{type = "label", caption = {"", {"required_kill_score"}, {"colon"}, " ", util.format_number(script_data.config.victory.kill_score.required_kill_score)}}
  end

  if script_data.config.game_config.time_limit > 0 then
    frame.add{type = "label", caption = {"time_left", get_time_left()}}
  end

  local inner_frame = frame.add{type = "frame", style = "inside_shallow_frame", direction = "vertical"}
  inner_frame.style.left_padding = 8
  inner_frame.style.top_padding = 8
  inner_frame.style.right_padding = 8
  inner_frame.style.bottom_padding = 8
  local information_table = inner_frame.add{type = "table", column_count = 3}
  information_table.draw_horizontal_line_after_headers = true
  information_table.draw_vertical_lines = true
  information_table.style.horizontal_spacing = 16
  information_table.style.vertical_spacing = 8
  information_table.style.column_alignments[3] = "right"

  for k, caption in pairs ({"", "team-name", "kill_score"}) do
    local label = information_table.add{type = "label", caption = {caption}}
    label.style.font = "default-bold"
  end
  local team_map = {}
  for k, team in pairs (script_data.config.teams) do
    team_map[team.name] = team
  end
  local scores = get_kill_scores()
  local rank = 1
  for name, score in spairs (scores, function(t, a, b) return t[b] < t[a] end) do
    if team_map[name] then
      local position = information_table.add{type = "label", caption = "#"..rank}
      if name == player.force.name then
        position.style.font = "default-semibold"
        position.style.font_color = {r = 1, g = 1}
      end
      local label = information_table.add{type = "label", caption = name}
      label.style.font = "default-semibold"
      label.style.font_color = get_color(team_map[name], true)
      information_table.add{type = "label", caption = util.format_number(math.floor(score))}
      rank = rank + 1
    end
  end
end

function update_space_race_frame(player)
  local frame = script_data.elements.space_race_frame[player.index]
  if not (frame and frame.valid) then
    return
  end
  frame.clear()

  if script_data.config.victory.space_race.required_rockets_sent > 0 then
    frame.add{type = "label", caption = {"", {"required_rockets_sent"}, {"colon"}, " ", util.format_number(script_data.config.victory.space_race.required_rockets_sent)}}
  end

  if script_data.config.game_config.time_limit > 0 then
    frame.add{type = "label", caption = {"time_left", get_time_left()}}
  end

  local inner_frame = frame.add{type = "frame", style = "inside_shallow_frame", direction = "vertical"}
  inner_frame.style.left_padding = 8
  inner_frame.style.top_padding = 8
  inner_frame.style.right_padding = 8
  inner_frame.style.bottom_padding = 8
  local information_table = inner_frame.add{type = "table", column_count = 3}
  information_table.draw_horizontal_line_after_headers = true
  information_table.draw_vertical_lines = true
  information_table.style.horizontal_spacing = 16
  information_table.style.vertical_spacing = 8
  information_table.style.column_alignments[3] = "right"

  for k, caption in pairs ({"", "team-name", "rockets_sent"}) do
    local label = information_table.add{type = "label", caption = {caption}}
    label.style.font = "default-bold"
  end
  local colors = {}
  local team_map = {}
  for k, team in pairs (script_data.config.teams) do
    colors[team.name] = get_color(team, true)
    team_map[team.name] = team
  end
  local rank = 1

  for name, score in spairs (get_rocket_scores(), function(t, a, b) return t[b] < t[a] end) do
    if team_map[name] then
      local position = information_table.add{type = "label", caption = "#"..rank}
      if name == player.force.name then
        position.style.font = "default-semibold"
        position.style.font_color = {r = 1, g = 1}
      end
      local label = information_table.add{type = "label", caption = name}
      label.style.font = "default-semibold"
      label.style.font_color = colors[name]
      information_table.add{type = "label", caption = util.format_number(score)}
      rank = rank + 1
    end
  end
end

function update_teams_names()
  local names = {}
  for k, team in pairs (script_data.config.teams) do
    names[team.name] = true
  end
  script_data.team_names = names
end

function setup_teams()

  local old_team_names = script_data.team_names
  update_teams_names()

  for name, bool in pairs (old_team_names) do
    if not script_data.team_names[name] then
      game.merge_forces(name, "player")
    end
  end

  for k, team in pairs (script_data.config.teams) do
    local new_team
    if game.forces[team.name] then
      new_team = game.forces[team.name]
    else
      new_team = game.create_force(team.name)
    end
    new_team.reset()
    new_team.set_spawn_position(script_data.spawn_positions[k], script_data.surface)
    set_random_team(team)
  end
  for k, team in pairs (script_data.config.teams) do
    local force = game.forces[team.name]
    set_diplomacy(team)
    setup_research(force)
    balance.disable_combat_technologies(force)
    force.reset_technology_effects()
    balance.apply_combat_modifiers(force)
  end
  disable_items_for_all()
end

function disable_items_for_all()
  if not script_data.config.disabled_items then return end
  local items = game.item_prototypes
  local recipes = game.recipe_prototypes
  local product_map = {}
  for k, recipe in pairs (recipes) do
    for k, product in pairs (recipe.products) do
      if not product_map[product.name] then
        product_map[product.name] = {}
      end
      insert(product_map[product.name], recipe)
    end
  end

  local recipes_to_disable = {}
  for name, k in pairs (script_data.config.disabled_items) do
    local mapping = product_map[name]
    if mapping then
      for k, recipe in pairs (mapping) do
        recipes_to_disable[recipe.name] = true
      end
    end
  end
  for k, force in pairs (game.forces) do
    for name, bool in pairs (recipes_to_disable) do
      force.recipes[name].enabled = false
    end
  end
end

function check_technology_for_disabled_items(event)
  if not script_data.config.disabled_items then return end
  local disabled_items = script_data.config.disabled_items
  local technology = event.research
  local recipes = technology.force.recipes
  for k, effect in pairs (technology.effects) do
    if effect.type == "unlock-recipe" then
      for k, product in pairs (recipes[effect.recipe].products) do
        if disabled_items[product.name] then
          recipes[effect.recipe].enabled = false
        end
      end
    end
  end
end

function set_random_team(team)
  if tonumber(team.team) then return end
  if team.team == "-" then return end
  team.team = "?"..math.random(#script_data.config.teams)
end

function set_diplomacy(team)
  local force = game.forces[team.name]
  if not force or not force.valid then return end
  local team_number
  if tonumber(team.team) then
    team_number = team.team
  elseif team.team:find("?") then
    team_number = team.team:gsub("?", "")
    team_number = tonumber(team_number)
  else
    team_number = "Don't match me"
  end
  for k, other_team in pairs (script_data.config.teams) do
    if game.forces[other_team.name] then
      local other_number
      if tonumber(other_team.team) then
        other_number = other_team.team
      elseif other_team.team:find("?") then
        other_number = other_team.team:gsub("?", "")
        other_number = tonumber(other_number)
      else
        other_number = "Okay i won't match"
      end
      if other_number == team_number then
        force.set_cease_fire(other_team.name, true)
        force.set_friend(other_team.name, true)
      else
        force.set_cease_fire(other_team.name, false)
        force.set_friend(other_team.name, false)
      end
    end
  end
end

function set_team_together_spawns(surface)
  local grouping = {}
  for k, team in pairs (script_data.config.teams) do
    local team_number
    if tonumber(team.team) then
      team_number = team.team
    elseif team.team:find("?") then
      team_number = team.team:gsub("?", "")
      team_number = tonumber(team_number)
    else
      team_number = "-"
    end
    if tonumber(team_number) then
      if not grouping[team_number] then
        grouping[team_number] = {}
      end
      insert(grouping[team_number], team.name)
    else
      if not grouping.no_group then
        grouping.no_group = {}
      end
      insert(grouping.no_group, team.name)
    end
  end
  local count = 1
  for k, group in pairs (grouping) do
    for j, team_name in pairs (group) do
      local force = game.forces[team_name]
      if force then
        local position = script_data.spawn_positions[count]
        if position then
          force.set_spawn_position(position, surface)
          count = count + 1
        end
      end
    end
  end
end

function chart_starting_area_for_force_spawns()
  --Delay by 1 tick so the GUI can update
  script_data.chart_chunks = 1 + game.tick + (#script_data.config.teams)
  script_data.progress = 0
  update_progress_bar()
end

function clear_biters(surface, area)
  for k, entity in pairs(surface.find_entities_filtered{force = "enemy", area = area}) do
    entity.destroy()
  end
end

function clear_cliffs(surface, area)
  for k, entity in pairs(surface.find_entities_filtered{type = "cliff", area = area}) do
    entity.destroy()
  end
end

function check_starting_area_chunks_are_generated()
  if not script_data.chart_chunks then return end
  local index = script_data.chart_chunks - game.tick
  local surface = script_data.surface
  if index == 0 then
    script_data.progress = 0.99
    script_data.chart_chunks = nil
    script_data.finish_setup = game.tick + (#script_data.config.teams)
    update_progress_bar()
    return
  end
  local team = script_data.config.teams[index]
  if not team then return end
  local name = team.name
  local force = game.forces[name]
  if not force then return end
  script_data.progress = (#script_data.config.teams - index) / #script_data.config.teams
  update_progress_bar()
  local surface = script_data.surface
  local radius = get_starting_area_radius() + 3
  local size = radius * 32
  local origin = force.get_spawn_position(surface)
  local area = {{origin.x - size, origin.y - size},{origin.x + (size - 32), origin.y + (size - 32)}}
  surface.request_to_generate_chunks(origin, radius)
  surface.force_generate_chunk_requests()
  clear_biters(surface, area)
  clear_cliffs(surface, area)
end

function check_player_color()
  for k, team in pairs (script_data.config.teams) do
    local force = game.forces[team.name]
    if force then
      local color = get_color(team)
      for k, player in pairs (force.connected_players) do
        local player_color = player.color
        for c, v in pairs (color) do
          if math.abs(player_color[c] - v) > 0.1 then
            game.print({"player-color-changed-back", player.name})
            player.color = color
            player.chat_color = get_color(team, true)
            break
          end
        end
      end
    end
  end
end

function check_no_rush()
  if not script_data.end_no_rush then return end
  if game.tick > script_data.end_no_rush then
    if script_data.config.game_config.no_rush_time > 0 then
      game.print({"no-rush-ends"})
    end
    script_data.end_no_rush = nil
    script_data.surface.peaceful_mode = script_data.peaceful_mode
    game.forces.enemy.kill_all_units()
    return
  end
end

function check_player_no_rush(player)
  if not script_data.end_no_rush then return end
  local force = player.force
  if not is_ignored_force(force.name) then
    local origin = force.get_spawn_position(player.surface)
    local Xo = origin.x
    local Yo = origin.y
    local position = player.position
    local radius = get_starting_area_radius(true)
    local Xp = position.x
    local Yp = position.y
    if Xp > (Xo + radius) then
      Xp = Xo + radius
    elseif Xp < (Xo - radius) then
      Xp = Xo - radius
    end
    if Yp > (Yo + radius) then
      Yp = Yo + radius
    elseif Yp < (Yo - radius) then
      Yp = Yo - radius
    end
    if position.x ~= Xp or position.y ~= Yp then
      local new_position = {x = Xp, y = Yp}
      local vehicle = player.vehicle
      if vehicle then
        if not vehicle.teleport(new_position) then
          player.driving = false
        end
        vehicle.orientation = vehicle.orientation + 0.5
      else
        player.teleport(new_position)
      end
      local time_left = math.ceil((script_data.end_no_rush-game.tick) / 3600)
      player.print({"no-rush-teleport", time_left})
    end
  end
end

function check_update_production_score()
  if not script_data.config.victory.production_score.active then return end
  local tick = game.tick
  if script_data.team_won then return end
  local new_scores = production_score.get_production_scores(script_data.price_list)
  local scale = statistics_period / 60
  local index = tick % (60 * statistics_period)

  if not (script_data.scores and script_data.average_score) then
    local average_score = {}
    local scores = {}
    for name, score in pairs (new_scores) do
      scores[name] = {}
      average_score[name] = score * statistics_period
      for k = 0, statistics_period do
        scores[name][k * 60] = score
      end
    end
    script_data.scores = scores
    script_data.average_score = average_score
  end

  local scores = script_data.scores
  local average_score = script_data.average_score
  for name, score in pairs (new_scores) do
    local team_score = scores[name] or {}
    local old_amount = team_score[index]
    if old_amount then
      average_score[name] = (average_score[name] + score) - old_amount
      scores[name][index] = score
    else
      --Something went wrong, reinitialize it next update
      script_data.scores = nil
      script_data.average_score = nil
      return check_update_production_score()
    end
  end

  script_data.production_scores = new_scores

  for k, player in pairs (game.connected_players) do
    update_production_score_frame(player)
  end
  local required = script_data.config.victory.production_score.required_production_score
  if required > 0 then
    for team_name, score in pairs (script_data.production_scores) do
      if score >= required then
        team_won(team_name)
      end
    end
  end
  if script_data.config.game_config.time_limit > 0 and tick > script_data.round_start_tick + (script_data.config.game_config.time_limit * 60 * 60) then
    local winner = {"none"}
    local winning_score = 0
    for team_name, score in pairs (script_data.production_scores) do
      if score > winning_score then
        winner = team_name
        winning_score = score
      end
    end
    team_won(winner)
  end
end

function check_update_oil_harvest_score()
  if script_data.team_won then return end
  if not script_data.config.victory.oil_harvest.active then return end
  local fluid_to_check = script_data.config.prototypes.oil or ""
  if not game.fluid_prototypes[fluid_to_check] then
    log("Disabling oil harvest check as "..fluid_to_check.." is not a valid fluid")
    script_data.config.victory.oil_harvest.active = false
    return
  end
  local scores = {}
  for force_name, force in pairs (game.forces) do
    local statistics = force.fluid_production_statistics
    local input = statistics.get_input_count(fluid_to_check)
    --local output = statistics.get_output_count(fluid_to_check)
    scores[force_name] = input
  end
  script_data.oil_harvest_scores = scores
  for k, player in pairs (game.connected_players) do
    update_oil_harvest_frame(player)
  end
  local required = script_data.config.victory.oil_harvest.required_oil
  if required > 0 then
    for team_name, score in pairs (script_data.oil_harvest_scores) do
      if score >= required then
        team_won(team_name)
      end
    end
  end
  if script_data.config.game_config.time_limit > 0 and game.tick > (script_data.round_start_tick + (script_data.config.game_config.time_limit * 60 * 60)) then
    local winner = {"none"}
    local winning_score = 0
    for team_name, score in pairs (script_data.oil_harvest_scores) do
      if score > winning_score then
        winner = team_name
        winning_score = score
      end
    end
    team_won(winner)
  end
end
function check_update_kill_score()
  if script_data.team_won then return end
  if not script_data.config.victory.kill_score.active then return end
  local scores = get_kill_scores()

  for k, player in pairs (game.connected_players) do
    update_kill_score_frame(player)
  end

  local required = script_data.config.victory.kill_score.required_kill_score
  if required > 0 then
    for team_name, score in pairs (scores) do
      if score >= required then
        team_won(team_name)
      end
    end
  end

  if script_data.config.game_config.time_limit > 0 and game.tick > (script_data.round_start_tick + (script_data.config.game_config.time_limit * 60 * 60)) then
    local winner = {"none"}
    local winning_score = 0
    for team_name, score in pairs (scores) do
      if score > winning_score then
        winner = team_name
        winning_score = score
      end
    end
    team_won(winner)
  end
end

function check_update_space_race_score()
  if script_data.team_won then return end
  if not script_data.config.victory.space_race.active then return end

  local scores = get_rocket_scores()
  for k, player in pairs (game.connected_players) do
    update_space_race_frame(player)
  end

  local required = script_data.config.victory.space_race.required_rockets_sent
  if required > 0 then
    for team_name, score in pairs (scores) do
      if score >= required then
        team_won(team_name)
      end
    end
  end
  if script_data.config.game_config.time_limit > 0 and game.tick > (script_data.round_start_tick + (script_data.config.game_config.time_limit * 60 * 60)) then
    local winner = {"none"}
    local winning_score = 0
    for team_name, score in pairs (scores) do
      if score > winning_score then
        winner = team_name
        winning_score = score
      end
    end
    team_won(winner)
  end
end

function finish_setup()
  if not script_data.finish_setup then return end
  local index = script_data.finish_setup - game.tick
  local surface = script_data.surface
  if index == 0 then
    final_setup_step()
    return
  end
  local name = script_data.config.teams[index].name
  if not name then return end
  local force = game.forces[name]
  if not force then return end
  duplicate_starting_area_entities(index)
  force.chart(surface, get_force_area(force))
  if script_data.config.game_config.reveal_team_positions then
    for name, other_force in pairs (game.forces) do
      if not is_ignored_force(name) then
        force.chart(surface, get_force_area(other_force))
      end
    end
  end
  create_silo_for_force(force)
  create_wall_for_force(force)
  create_moat_for_force(force)
  create_starting_chest(force)
  create_starting_turrets(force)
  create_starting_artillery(force)
  protect_force_area(force)
  force.friendly_fire = script_data.config.team_config.friendly_fire
  force.share_chart = true
end

function get_kill_scores()
  if script_data.kill_scores then return script_data.kill_scores end
  local scores = {}
  for k, force in pairs (game.forces) do
    scores[force.name] = 0
  end
  script_data.kill_scores = scores
  return scores
end

function get_rocket_scores()
  if script_data.rocket_scores then return script_data.rocket_scores end
  local scores = {}
  for k, force in pairs (game.forces) do
    scores[force.name] = 0
  end
  script_data.rocket_scores = scores
  return scores
end

function final_setup_step()
  script_data.progress = 1
  update_progress_bar()
  create_exclusion_map()
  script_data.progress = nil
  local surface = script_data.surface
  script_data.finish_setup = nil
  game.print({"map-ready"})
  script_data.setup_finished = true
  script_data.round_start_tick = game.tick
  for k, player in pairs (game.connected_players) do
    destroy_player_gui(player)
    player.teleport({0, 1000}, get_lobby_surface())
    local team = script_data.team_players[player.index]
    if team and script_data.team_names[team.name] and game.forces[team.name] then
      set_player(player, team, true)
    else
      script_data.team_players[player.index] = nil
      choose_joining_gui(player)
    end
  end
  if script_data.config.game_config.no_rush_time then
    script_data.end_no_rush = game.tick + (script_data.config.game_config.no_rush_time * 60 * 60)
    if script_data.config.game_config.no_rush_time > 0 then
      script_data.peaceful_mode = script_data.surface.peaceful_mode
      script_data.surface.peaceful_mode = true
      game.forces.enemy.kill_all_units()
      game.print({"no-rush-begins", script_data.config.game_config.no_rush_time})
    end
  end
  if script_data.config.game_config.base_exclusion_time then
    if script_data.config.game_config.base_exclusion_time > 0 then
      script_data.check_base_exclusion = true
      game.print({"base-exclusion-begins", script_data.config.game_config.base_exclusion_time})
    end
  end
  if script_data.config.game_config.reveal_map_center then
    local radius = (script_data.config.team_config.average_team_displacement / 2) + get_starting_area_radius(true)
    local origin = script_data.spawn_offset
    local area = {{origin.x - radius, origin.y - radius}, {origin.x + (radius - 32), origin.y + (radius - 32)}}
    for k, force in pairs (game.forces) do
      force.chart(surface, area)
    end
  end
  script_data.oil_harvest_scores = {}
  script_data.production_scores = {}
  get_rocket_scores()
  get_kill_scores()
  if script_data.config.victory.production_score.active then
    script_data.price_list = script_data.price_list or production_score.generate_price_list()
  end
  if script_data.config.team_config.defcon_mode then
    defcon_research()
  end

  script.raise_event(events.on_round_start, {})

end

function check_force_protection(force)
  if not script_data.config.game_config.protect_empty_teams then return end
  if not (force and force.valid) then return end
  if is_ignored_force(force.name) then return end
  if not script_data.protected_teams then script_data.protected_teams = {} end
  local protected = script_data.protected_teams[force.name] ~= nil
  local should_protect = #force.connected_players == 0
  if protected and should_protect then return end
  if (not protected) and (not should_protect) then return end
  if protected and (not should_protect) then
    unprotect_force_area(force)
    return
  end
  if (not protected) and should_protect then
    protect_force_area(force)
    check_base_exclusion()
    return
  end
end

function protect_force_area(force)
  if not script_data.config.game_config.protect_empty_teams then return end
  local surface = script_data.surface
  if not (surface and surface.valid) then return end
  local non_destructible = {}
  for k, entity in pairs (surface.find_entities_filtered{force = force, area = get_force_area(force)}) do
    if entity.destructible == false and entity.unit_number then
      non_destructible[entity.unit_number] = true
    end
    entity.destructible = false
  end
  if not script_data.protected_teams then
    script_data.protected_teams = {}
  end
  script_data.protected_teams[force.name] = non_destructible
end

function unprotect_force_area(force)
  if not script_data.config.game_config.protect_empty_teams then return end
  local surface = script_data.surface
  if not (surface and surface.valid) then return end
  if not script_data.protected_teams then
    script_data.protected_teams = {}
  end
  local entities = script_data.protected_teams[force.name] or {}
  for k, entity in pairs (surface.find_entities_filtered{force = force, area = get_force_area(force)}) do
    if (not entity.unit_number) or (not entities[entity.unit_number]) then
      entity.destructible = true
    end
  end
  script_data.protected_teams[force.name] = nil
end

function get_force_area(force)
  if not (force and force.valid) then return end
  local surface = script_data.surface
  if not (surface and surface.valid) then return end
  local radius = get_starting_area_radius(true)
  local origin = force.get_spawn_position(surface)
  return {{origin.x - radius, origin.y - radius}, {origin.x + (radius - 1), origin.y + (radius - 1)}}
end

function update_progress_bar()
  if not script_data.progress then return end
  local percent = script_data.progress
  local finished = (percent >=1)
  function update_bar_gui(player)
    local frame = script_data.elements.progress_bar[player.index]
    if frame and frame.valid then
      script_data.elements.progress_bar[player.index] = nil
      frame.destroy()
    end
    if finished then return end
    local frame = player.gui.center.add{type = "frame", caption = {"progress-bar"}}
    script_data.elements.progress_bar[player.index] = frame
    frame.add{type = "progressbar", size = 100, value = percent}
  end
  for k, player in pairs (game.connected_players) do
    update_bar_gui(player)
  end
  if finished then
    script_data.progress = nil
    script_data.setup_duration = nil
    script_data.finish_tick = nil
  end
end

function create_silo_for_force(force)
  if not script_data.config.victory.last_silo_standing.active then return end
  if not (force and force.valid) then return end
  local surface = script_data.surface
  local origin = force.get_spawn_position(surface)
  local offset = script_data.config.silo_offset
  local silo_position = {x = origin.x + (offset.x or offset[1]), y = origin.y + (offset.y or offset[2])}
  local silo_name = script_data.config.prototypes.silo
  if not game.entity_prototypes[silo_name] then log("Silo not created as "..silo_name.." is not a valid entity prototype") return end
  local silo = surface.create_entity{name = silo_name, position = silo_position, force = force, raise_built = true, create_build_effect_smoke = false}
  --Event is sent, so some mod could kill the silo
  if not (silo and silo.valid) then return end

  silo.minable = false
  if silo.supports_backer_name() then
    silo.backer_name = force.name
  end
  if not script_data.silos then script_data.silos = {} end
  script_data.silos[force.name] = silo

  local tile_name = script_data.config.prototypes.tile_2
  if not game.tile_prototypes[tile_name] then tile_name = get_walkable_tile() end

  local tiles_2 = {}
  local box = silo.bounding_box
  local x1, x2, y1, y2 =
    math.floor(box.left_top.x) - 1,
    math.floor(box.right_bottom.x) + 1,
    math.floor(box.left_top.y) - 1,
    math.floor(box.right_bottom.y) + 1
  for X = x1, x2 do
    for Y = y1, y2 do
      insert(tiles_2, {name = tile_name, position = {X, Y}})
    end
  end

  for i, entity in pairs(surface.find_entities_filtered({area = {{x1 - 1, y1 - 1},{x2 + 1, y2 + 1}}, force = "neutral"})) do
    entity.destroy()
  end

  set_tiles_safe(surface, tiles_2)
end

function setup_research(force)
  if not script_data.config.team_config.research_level then return end
  if not (force and force.valid) then return end
  local tier = script_data.config.team_config.research_level.selected
  local index
  local set = (tier ~= "none")
  for k, name in pairs (script_data.config.team_config.research_level.options) do
    if script_data.config.research_ingredient_list[name] ~= nil then
      script_data.config.research_ingredient_list[name] = set
    end
    if name == tier then set = false end
  end
  --[[Unlocks all research, and then unenables them based on a blacklist]]
  force.research_all_technologies()
  for k, technology in pairs (force.technologies) do
    for j, ingredient in pairs (technology.research_unit_ingredients) do
      if not script_data.config.research_ingredient_list[ingredient.name] then
        technology.researched = false
        break
      end
    end
  end
end

function create_starting_turrets(force)
  if not script_data.config.game_config.team_turrets then return end
  if not (force and force.valid) then return end
  local turret_name = script_data.config.prototypes.turret
  if not game.entity_prototypes[turret_name] then return end

  local ammo_name
  if script_data.config.game_config.turret_ammunition then
    ammo_name = script_data.config.game_config.turret_ammunition.selected
  end
  local insert = insert
  local direction = defines.direction
  local surface = script_data.surface
  local height = surface.map_gen_settings.height / 2
  local width = surface.map_gen_settings.width / 2
  local origin = force.get_spawn_position(surface)
  local radius = get_starting_area_radius(true) - 18 --[[radius in tiles]]
  local positions = {}
  local Xo = origin.x
  local Yo = origin.y
  for X = -radius, radius do
    local Xt = X + Xo
    if X == -radius then
      for Y = -radius, radius do
        local Yt = Y + Yo
        if (Yt + 16) % 32 ~= 0 and Yt % 8 == 0 then
          insert(positions, {x = Xo - radius, y = Yt, direction = direction.west})
          insert(positions, {x = Xo + radius, y = Yt, direction = direction.east})
        end
      end
    elseif (Xt + 16) % 32 ~= 0 and Xt % 8 == 0 then
      insert(positions, {x = Xt, y = Yo - radius, direction = direction.north})
      insert(positions, {x = Xt, y = Yo + radius, direction = direction.south})
    end
  end
  local tiles = {}
  local tile_name = script_data.config.prototypes.tile_2
  if not game.tile_prototypes[tile_name] then tile_name = get_walkable_tile() end
  local stack
  if ammo_name and game.item_prototypes[ammo_name] then
    stack = {name = ammo_name, count = 20}
  end
  local find_entities_filtered = surface.find_entities_filtered
  local neutral = game.forces.neutral
  local destroy_params = {do_cliff_correction = true}
  local floor = math.floor
  local create_entity = surface.create_entity
  for k, position in pairs (positions) do
    if is_in_map(width, height, position) then
      local turret = create_entity{name = turret_name, position = position, force = force, direction = position.direction, create_build_effect_smoke = false}
      local box = turret.bounding_box
      for k, entity in pairs (find_entities_filtered{area = turret.bounding_box, force = neutral}) do
        entity.destroy(destroy_params)
      end
      if stack then
        turret.insert(stack)
      end
      for x = floor(box.left_top.x), floor(box.right_bottom.x) do
        for y = floor(box.left_top.y), floor(box.right_bottom.y) do
          insert(tiles, {name = tile_name, position = {x, y}})
        end
      end
    end
  end
  set_tiles_safe(surface, tiles)
end

function create_starting_artillery(force)
  if not script_data.config.game_config.team_artillery then return end
  if not (force and force.valid) then return end
  local turret_name = script_data.config.prototypes.artillery
  if not (turret_name and game.entity_prototypes[turret_name]) then return end
  local ammo_name = script_data.config.prototypes.artillery_ammo
  if not (ammo_name and game.item_prototypes[ammo_name]) then return end
  local surface = script_data.surface
  local height = surface.map_gen_settings.height / 2
  local width = surface.map_gen_settings.width / 2
  local origin = force.get_spawn_position(surface)
  local radius = get_starting_area_radius() - 1 --[[radius in chunks]]
  if radius < 1 then return end
  local positions = {}
  local tile_positions = {}
  for x = -radius, 0 do
    if x == -radius then
      for y = -radius, 0 do
        insert(positions, {x = 1 + origin.x + 32 * x, y = 1 + origin.y + 32 * y})
      end
    else
      insert(positions, {x = 1 + origin.x + 32 * x, y = 1 + origin.y - radius * 32})
    end
  end
  for x = 1, radius do
    if x == radius then
      for y = -radius, -1 do
        insert(positions, {x = -2 + origin.x + 32 * x, y = 1 + origin.y + 32 * y})
      end
    else
      insert(positions, {x = -2 + origin.x + 32 * x, y = 1 + origin.y - radius * 32})
    end
  end
  for x = -radius, -1 do
    if x == -radius then
      for y = 1, radius do
        insert(positions, {x = 1 + origin.x + 32 * x, y = -2 + origin.y + 32 * y})
      end
    else
      insert(positions, {x = 1 + origin.x + 32 * x, y = -2 + origin.y + radius * 32})
    end
  end
  for x = 0, radius do
    if x == radius then
      for y = 0, radius do
        insert(positions, {x = -2 + origin.x + 32*  x, y = -2 + origin.y + 32 * y})
      end
    else
      insert(positions, {x = -2 + origin.x + 32 * x, y = -2 + origin.y + radius * 32})
    end
  end
  local stack = {name = ammo_name, count = 20}
  local tiles = {}
  local tile_name = script_data.config.prototypes.tile_2
  if not game.tile_prototypes[tile_name] then tile_name = get_walkable_tile() end
  local floor = math.floor
  for k, position in pairs (positions) do
    if is_in_map(width, height, position) then
      local turret = surface.create_entity{name = turret_name, position = position, force = force, direction = position.direction, create_build_effect_smoke = false}
      local box = turret.bounding_box
      for k, entity in pairs (surface.find_entities_filtered{area = turret.bounding_box, force = "neutral"}) do
        entity.destroy({do_cliff_correction = true})
      end
      turret.insert(stack)
      for x = floor(box.left_top.x), floor(box.right_bottom.x) do
        for y = floor(box.left_top.y), floor(box.right_bottom.y) do
          insert(tiles, {name = tile_name, position = {x, y}})
        end
      end
    end
  end
  set_tiles_safe(surface, tiles)
end

function create_moat_for_force(force)
  if not script_data.config.game_config.team_moat then
    return
  end

  local tile_name = script_data.config.prototypes.moat
  if not game.tile_prototypes[tile_name] then
    return
  end

  if not force.valid then return end
  local surface = script_data.surface
  local height = surface.map_gen_settings.height / 2
  local width = surface.map_gen_settings.width / 2
  local origin = force.get_spawn_position(surface)
  local radius = get_starting_area_radius(true)

  local tiles = {}
  local water_radius = radius + 6
  for X = -water_radius, water_radius - 1 do
    if X >= 18 or X < -18 then
      for k = 0, 11 do
        insert(tiles, {name = tile_name, position = {x = origin.x + X, y = origin.y - water_radius + k}})
        insert(tiles, {name = tile_name, position = {x = origin.x + X, y = origin.y + (water_radius-1) - k}})
        insert(tiles, {name = tile_name, position = {x = origin.x - water_radius + k, y = origin.y + X}})
        insert(tiles, {name = tile_name, position = {x = origin.x + (water_radius-1) - k, y = origin.y + X}})
      end
    end
  end

  surface.set_tiles(tiles)

  local cliff_radius = radius - 6

  --The corners

  surface.create_entity{name = "cliff", position = {x = origin.x - cliff_radius, y = origin.y - cliff_radius}, cliff_orientation = "east-to-south"}
  surface.create_entity{name = "cliff", position = {x = origin.x + cliff_radius, y = origin.y - cliff_radius}, cliff_orientation = "south-to-west"}
  surface.create_entity{name = "cliff", position = {x = origin.x - cliff_radius, y = origin.y + cliff_radius}, cliff_orientation = "north-to-east"}
  surface.create_entity{name = "cliff", position = {x = origin.x + cliff_radius, y = origin.y + cliff_radius}, cliff_orientation = "west-to-north"}

  --The lengths

  for k = -(cliff_radius - 4), (cliff_radius - 4), 4 do

    if k >= 20 or k < -20 then
      surface.create_entity{name = "cliff", position = {x = origin.x + k, y = origin.y - cliff_radius}, cliff_orientation = "east-to-west"}
      surface.create_entity{name = "cliff", position = {x = origin.x + k, y = origin.y + cliff_radius}, cliff_orientation = "west-to-east"}
      surface.create_entity{name = "cliff", position = {x = origin.x - cliff_radius, y = origin.y + k }, cliff_orientation = "north-to-south"}
      surface.create_entity{name = "cliff", position = {x = origin.x + cliff_radius, y = origin.y + k}, cliff_orientation = "south-to-north"}
    end
  end

  -- The openings

  --Bottom
  surface.create_entity{name = "cliff", position = {x = origin.x - 18, y = origin.y + cliff_radius}, cliff_orientation = "west-to-south"}
  surface.create_entity{name = "cliff", position = {x = origin.x - 18, y = origin.y + cliff_radius + 4}, cliff_orientation = "north-to-south"}
  surface.create_entity{name = "cliff", position = {x = origin.x - 18, y = origin.y + cliff_radius + 8}, cliff_orientation = "north-to-south"}
  surface.create_entity{name = "cliff", position = {x = origin.x - 18, y = origin.y + cliff_radius + 12}, cliff_orientation = "north-to-none"}

  surface.create_entity{name = "cliff", position = {x = origin.x + 18, y = origin.y + cliff_radius}, cliff_orientation = "south-to-east"}
  surface.create_entity{name = "cliff", position = {x = origin.x + 18, y = origin.y + cliff_radius + 4}, cliff_orientation = "south-to-north"}
  surface.create_entity{name = "cliff", position = {x = origin.x + 18, y = origin.y + cliff_radius + 8}, cliff_orientation = "south-to-north"}
  surface.create_entity{name = "cliff", position = {x = origin.x + 18, y = origin.y + cliff_radius + 12}, cliff_orientation = "none-to-north"}

  --Top
  surface.create_entity{name = "cliff", position = {x = origin.x - 18, y = origin.y - cliff_radius}, cliff_orientation = "north-to-west"}
  surface.create_entity{name = "cliff", position = {x = origin.x - 18, y = origin.y - cliff_radius - 4}, cliff_orientation = "north-to-south"}
  surface.create_entity{name = "cliff", position = {x = origin.x - 18, y = origin.y - cliff_radius - 8}, cliff_orientation = "north-to-south"}
  surface.create_entity{name = "cliff", position = {x = origin.x - 18, y = origin.y - cliff_radius - 12}, cliff_orientation = "none-to-south"}

  surface.create_entity{name = "cliff", position = {x = origin.x + 18, y = origin.y - cliff_radius}, cliff_orientation = "east-to-north"}
  surface.create_entity{name = "cliff", position = {x = origin.x + 18, y = origin.y - cliff_radius - 4}, cliff_orientation = "south-to-north"}
  surface.create_entity{name = "cliff", position = {x = origin.x + 18, y = origin.y - cliff_radius - 8}, cliff_orientation = "south-to-north"}
  surface.create_entity{name = "cliff", position = {x = origin.x + 18, y = origin.y - cliff_radius - 12}, cliff_orientation = "south-to-none"}

  --Right
  surface.create_entity{name = "cliff", position = {x = origin.x + cliff_radius, y = origin.y - 18}, cliff_orientation = "east-to-north"}
  surface.create_entity{name = "cliff", position = {x = origin.x + cliff_radius + 4, y = origin.y - 18}, cliff_orientation = "east-to-west"}
  surface.create_entity{name = "cliff", position = {x = origin.x + cliff_radius + 8, y = origin.y - 18}, cliff_orientation = "east-to-west"}
  surface.create_entity{name = "cliff", position = {x = origin.x + cliff_radius + 12, y = origin.y - 18}, cliff_orientation = "none-to-west"}

  surface.create_entity{name = "cliff", position = {x = origin.x + cliff_radius, y = origin.y + 18}, cliff_orientation = "south-to-east"}
  surface.create_entity{name = "cliff", position = {x = origin.x + cliff_radius + 4, y = origin.y + 18}, cliff_orientation = "west-to-east"}
  surface.create_entity{name = "cliff", position = {x = origin.x + cliff_radius + 8, y = origin.y + 18}, cliff_orientation = "west-to-east"}
  surface.create_entity{name = "cliff", position = {x = origin.x + cliff_radius + 12, y = origin.y + 18}, cliff_orientation = "west-to-none"}

  --Left
  surface.create_entity{name = "cliff", position = {x = origin.x - cliff_radius, y = origin.y - 18}, cliff_orientation = "north-to-west"}
  surface.create_entity{name = "cliff", position = {x = origin.x - cliff_radius -  4, y = origin.y - 18}, cliff_orientation = "east-to-west"}
  surface.create_entity{name = "cliff", position = {x = origin.x - cliff_radius - 8, y = origin.y - 18}, cliff_orientation = "east-to-west"}
  surface.create_entity{name = "cliff", position = {x = origin.x - cliff_radius - 12, y = origin.y - 18}, cliff_orientation = "east-to-none"}

  surface.create_entity{name = "cliff", position = {x = origin.x - cliff_radius, y = origin.y + 18}, cliff_orientation = "west-to-south"}
  surface.create_entity{name = "cliff", position = {x = origin.x - cliff_radius - 4, y = origin.y + 18}, cliff_orientation = "west-to-east"}
  surface.create_entity{name = "cliff", position = {x = origin.x - cliff_radius - 8, y = origin.y + 18}, cliff_orientation = "west-to-east"}
  surface.create_entity{name = "cliff", position = {x = origin.x - cliff_radius - 12, y = origin.y + 18}, cliff_orientation = "none-to-east"}

end

function create_wall_for_force(force)
  if not script_data.config.game_config.team_walls then return end
  if not force.valid then return end
  local surface = script_data.surface
  local height = surface.map_gen_settings.height / 2
  local width = surface.map_gen_settings.width / 2
  local origin = force.get_spawn_position(surface)
  local radius = get_starting_area_radius(true) - 11
  if radius < 2 then return end
  local perimeter_top = {}
  local perimeter_bottom = {}
  local perimeter_left = {}
  local perimeter_right = {}
  local tiles = {}
  local insert = insert
  for X = -radius, radius - 1 do
    insert(perimeter_top, {x = origin.x + X, y = origin.y - radius})
    insert(perimeter_bottom, {x = origin.x + X, y = origin.y + (radius-1)})
  end
  for Y = -radius, radius - 1 do
    insert(perimeter_left, {x = origin.x - radius, y = origin.y + Y})
    insert(perimeter_right, {x = origin.x + (radius-1), y = origin.y + Y})
  end
  local tile_name = script_data.config.prototypes.tile_1
  if not game.tile_prototypes[tile_name] then tile_name = get_walkable_tile() end
  local areas =
  {
    {{perimeter_top[1].x, perimeter_top[1].y - 1}, {perimeter_top[#perimeter_top].x, perimeter_top[1].y + 3}},
    {{perimeter_bottom[1].x, perimeter_bottom[1].y - 3}, {perimeter_bottom[#perimeter_bottom].x, perimeter_bottom[1].y + 1}},
    {{perimeter_left[1].x - 1, perimeter_left[1].y}, {perimeter_left[1].x + 3, perimeter_left[#perimeter_left].y}},
    {{perimeter_right[1].x - 3, perimeter_right[1].y}, {perimeter_right[1].x + 1, perimeter_right[#perimeter_right].y}}
  }
  local find_entities_filtered = surface.find_entities_filtered
  local destroy_param = {do_cliff_correction = true}
  for k, area in pairs (areas) do
    for i, entity in pairs(find_entities_filtered({area = area})) do
      entity.destroy(destroy_param)
    end
  end
  local wall_name = script_data.config.prototypes.wall
  local gate_name = script_data.config.prototypes.gate
  if not game.entity_prototypes[wall_name] then
    log("Setting walls cancelled as "..wall_name.." is not a valid entity prototype")
    return
  end
  if not game.entity_prototypes[gate_name] then
    log("Setting walls cancelled as "..gate_name.." is not a valid entity prototype")
    return
  end
  local should_gate =
  {
    [12] = true,
    [13] = true,
    [14] = true,
    [15] = true,
    [16] = true,
    [17] = true,
    [18] = true,
    [19] = true
  }
  local create_entity = surface.create_entity
  for k, position in pairs (perimeter_left) do
    if is_in_map(width, height, position) then
      if (k ~= 1) and (k ~= #perimeter_left) then
        insert(tiles, {name = tile_name, position = {position.x + 2, position.y}})
        insert(tiles, {name = tile_name, position = {position.x + 1, position.y}})
      end
      if should_gate[position.y % 32] then
        create_entity{name = gate_name, position = position, direction = 0, force = force, create_build_effect_smoke = false}
      else
        create_entity{name = wall_name, position = position, force = force, create_build_effect_smoke = false}
      end
    end
  end
  for k, position in pairs (perimeter_right) do
    if is_in_map(width, height, position) then
      if (k ~= 1) and (k ~= #perimeter_right) then
        insert(tiles, {name = tile_name, position = {position.x - 2, position.y}})
        insert(tiles, {name = tile_name, position = {position.x - 1, position.y}})
      end
      if should_gate[position.y % 32] then
        create_entity{name = gate_name, position = position, direction = 0, force = force, create_build_effect_smoke = false}
      else
        create_entity{name = wall_name, position = position, force = force, create_build_effect_smoke = false}
      end
    end
  end
  for k, position in pairs (perimeter_top) do
    if is_in_map(width, height, position) then
      if (k ~= 1) and (k ~= #perimeter_top) then
        insert(tiles, {name = tile_name, position = {position.x, position.y + 2}})
        insert(tiles, {name = tile_name, position = {position.x, position.y + 1}})
      end
      if should_gate[position.x % 32] then
        create_entity{name = gate_name, position = position, direction = 2, force = force, create_build_effect_smoke = false}
      else
        create_entity{name = wall_name, position = position, force = force, create_build_effect_smoke = false}
      end
    end
  end
  for k, position in pairs (perimeter_bottom) do
    if is_in_map(width, height, position) then
      if (k ~= 1) and (k ~= #perimeter_bottom) then
        insert(tiles, {name = tile_name, position = {position.x, position.y - 2}})
        insert(tiles, {name = tile_name, position = {position.x, position.y - 1}})
      end
      if should_gate[position.x % 32] then
        create_entity{name = gate_name, position = position, direction = 2, force = force, create_build_effect_smoke = false}
      else
        create_entity{name = wall_name, position = position, force = force, create_build_effect_smoke = false}
      end
    end
  end
  set_tiles_safe(surface, tiles)
end

function spairs(t, order)
  local keys = {}
  for k in pairs(t) do keys[#keys+1] = k end
  if order then
    table.sort(keys, function(a, b) return order(t, a, b) end)
  else
    table.sort(keys)
  end
  local i = 0
  return function()
    i = i + 1
    if keys[i] then
      return keys[i], t[keys[i]]
    end
  end
end

function areas_overlap(area_1, area_2)
  local left_top = area_1[1]
  local right_bottom = area_1[2]
  local x1 = area_2[1][1]
  local x2 = area_2[2][1]
  local y1 = area_2[1][2]
  local y2 = area_2[2][2]
  if x1 > left_top[1] and x1 < right_bottom[1] then return true end
  if x2 > left_top[1] and x2 < right_bottom[1] then return true end
  if y1 > left_top[2] and y1 < right_bottom[2] then return true end
  if y2 > left_top[2] and y2 < right_bottom[2] then return true end
  return false
end

function duplicate_starting_area_entities(index)
  if not script_data.config.team_config.duplicate_starting_area_entities then return end
  if index == 1 then return end --Index 1 is the copy force... so we don't copy anything...
  local copy_team = script_data.config.teams[1]
  if not copy_team then return end
  local force = game.forces[copy_team.name]
  if not force then return end
  local destination_team = script_data.config.teams[index]
  local destination_force = game.forces[destination_team.name]
  if not destination_force then return end
  local surface = script_data.surface
  local origin_spawn = force.get_spawn_position(surface)
  local radius = get_starting_area_radius(true) --[[radius in tiles]]
  local copy_area = {{origin_spawn.x - radius, origin_spawn.y - radius}, {origin_spawn.x + radius, origin_spawn.y + radius}}
  local destination_spawn = destination_force.get_spawn_position(surface)
  local destination_area = {{destination_spawn.x - radius, destination_spawn.y - radius}, {destination_spawn.x + radius, destination_spawn.y + radius}}

  local tile_name = get_walkable_tile()
  local top_count = 0
  for name, tile in pairs (game.tile_prototypes) do
    if not tile.collision_mask["resource-layer"] then
      local count = surface.count_tiles_filtered{name = name, area = destination_area}
      if count > top_count then
        top_count = count
        tile_name = name
      end
    end
  end

  local offset_x = destination_spawn.x - origin_spawn.x
  local offset_y = destination_spawn.y - origin_spawn.y

  --Fill in current water in destination area
  local set_tiles = {}
  local tile_count = 0
  for k, tile in pairs (surface.find_tiles_filtered{area = destination_area, collision_mask = "resource-layer"}) do
    tile_count = tile_count + 1
    set_tiles[tile_count] = {name = tile_name, position = {x = tile.position.x, y = tile.position.y}}
  end
  surface.set_tiles(set_tiles)

  --Copy water from copy area
  local set_water = {}
  local water_count = 0
  for k, tile in pairs (surface.find_tiles_filtered{area = copy_area, collision_mask = "resource-layer"}) do
    water_count = water_count + 1
    set_water[water_count] = {name = tile.name, position = {x = tile.position.x + offset_x, y = tile.position.y + offset_y}}
  end
  surface.set_tiles(set_water)

  local success = pcall(surface.clone_area,
  {
    source_area = copy_area,
    destination_area = destination_area,
    clone_entities = true,
    clear_destination = true,
    clone_tiles = false,
    clone_decoratives = true
  })

  if not success then
    game.print({"duplicate-failed"})
    log("Duplicating failed, probably due to poor map conditions")
  end

end

function create_starting_chest(force)
  if not (force and force.valid) then return end
  if not script_data.config.starting_chest then return end
  local value = script_data.config.starting_chest.selected
  local multiplier = script_data.config.team_config.starting_chest_multiplier
  if not (multiplier > 0) then return end
  local inventory = script_data.config.inventory_list[value]
  if not inventory then return end
  if not (table_size(inventory) > 0) then return end
  local surface = script_data.surface
  local chest_name = script_data.config.prototypes.chest
  local prototype = game.entity_prototypes[chest_name]
  if not prototype then
    log("Starting chest "..chest_name.." is not a valid entity prototype, picking a new container from prototype list")
    for name, chest in pairs (game.entity_prototypes) do
      if chest.type == "container" then
        chest_name = name
        prototype = chest
        break
      end
    end
  end
  local size = math.ceil(prototype.radius * 2)
  local origin = force.get_spawn_position(surface)
  local offset = script_data.config.chest_offset
  origin.x = origin.x + offset.x
  origin.y = origin.y + offset.y
  local index = 1
  local position = {x = origin.x + get_chest_offset(index).x * size, y = origin.y + get_chest_offset(index).y * size}
  local chest = surface.create_entity{name = chest_name, position = position, force = force, create_build_effect_smoke = false}
  for k, v in pairs (surface.find_entities_filtered{force = "neutral", area = chest.bounding_box}) do
    v.destroy()
  end
  local tiles = {}
  local grass = {}
  local tile_name = script_data.config.prototypes.tile_1
  if not game.tile_prototypes[tile_name] then tile_name = get_walkable_tile() end
  insert(tiles, {name = tile_name, position = {x = position.x, y = position.y}})
  chest.destructible = false
  local items = game.item_prototypes
  for name, count in pairs (inventory) do
    if items[name] then
      local count_to_insert = math.ceil(count * multiplier)
      local difference = count_to_insert - chest.insert{name = name, count = count_to_insert}
      while difference > 0 do
        index = index + 1
        position = {x = origin.x + get_chest_offset(index).x * size, y = origin.y + get_chest_offset(index).y * size}
        chest = surface.create_entity{name = chest_name, position = position, force = force, create_build_effect_smoke = false}
        for k, v in pairs (surface.find_entities_filtered{force = "neutral", area = chest.bounding_box}) do
          v.destroy()
        end
        insert(tiles, {name = tile_name, position = {x = position.x, y = position.y}})
        chest.destructible = false
        difference = difference - chest.insert{name = name, count = difference}
      end
    end
  end
  set_tiles_safe(surface, tiles)
end

local root_2 = 2 ^ 0.5

function get_chest_offset(n)
  local offset_x = 0
  n = n / 2
  if n % 1 == 0.5 then
    offset_x = -1
    n = n + 0.5
  end
  local root = n ^ 0.5
  local nearest_root = math.floor(root + 0.5)
  local upper_root = math.ceil(root)
  local root_difference = math.abs(nearest_root ^ 2 - n)
  if nearest_root == upper_root then
    x = upper_root - root_difference
    y = nearest_root
  else
    x = upper_root
    y = root_difference
  end
  local orientation = 2 * math.pi * (45/360)
  x = x * root_2
  y = y * root_2
  local rotated_x = math.floor(0.5 + x * math.cos(orientation) - y * math.sin(orientation))
  local rotated_y = math.floor(0.5 + x * math.sin(orientation) + y * math.cos(orientation))
  return {x = rotated_x + offset_x, y = rotated_y}
end

function set_tiles_safe(surface, tiles)
  local grass = get_walkable_tile()
  local grass_tiles = {}
  for k, tile in pairs (tiles) do
    grass_tiles[k] = {position = {x = (tile.position.x or tile.position[1]), y = (tile.position.y or tile.position[2])}, name = grass}
  end
  surface.set_tiles(grass_tiles, false)
  surface.set_tiles(tiles)
end

function create_exclusion_map()
  local surface = script_data.surface
  if not (surface and surface.valid) then return end
  local exclusion_map = {}
  local radius = get_starting_area_radius() --[[radius in chunks]]
  for k, team in pairs (script_data.config.teams) do
    local name = team.name
    local force = game.forces[name]
    if force then
      local origin = force.get_spawn_position(surface)
      local Xo = math.floor(origin.x / 32)
      local Yo = math.floor(origin.y / 32)
      for X = -radius, radius - 1 do
        Xb = X + Xo
        if not exclusion_map[Xb] then exclusion_map[Xb] = {} end
        for Y = -radius, radius - 1 do
          local Yb = Y + Yo
          exclusion_map[Xb][Yb] = name
        end
      end
    end
  end
  script_data.exclusion_map = exclusion_map
end

function check_base_exclusion()
  if not (script_data.check_base_exclusion or script_data.protected_teams) then return end

  if script_data.check_base_exclusion and game.tick > (script_data.round_start_tick + (script_data.config.game_config.base_exclusion_time * 60 * 60)) then
    script_data.check_base_exclusion = nil
    game.print({"base-exclusion-ends"})
  end

end

function check_player_base_exclusion(player)
  if not (script_data.check_base_exclusion or script_data.protected_teams) then return end

  if not is_ignored_force(player.force.name) then
    check_player_exclusion(player, get_chunk_map_position(player.position))
  end
end

function get_chunk_map_position(position)
  local map = script_data.exclusion_map
  local chunk_x = math.floor(position.x / 32)
  local chunk_y = math.floor(position.y / 32)
  if map[chunk_x] then
    return map[chunk_x][chunk_y]
  end
end

function is_ignored_force(name)
  return not script_data.team_names[name]
end

function check_player_exclusion(player, force_name)
  if not force_name then return end
  local force = game.forces[force_name]
  if not (force and force.valid and player and player.valid) then return end
  if force == player.force or force.get_friend(player.force) then return end
  if not (script_data.check_base_exclusion or (script_data.protected_teams and script_data.protected_teams[force_name])) then return end
  local surface = script_data.surface
  local origin = force.get_spawn_position(surface)
  local radius = get_starting_area_radius(true) --[[radius in tiles]]
  local position = {x = player.position.x, y = player.position.y}
  local vector = {x = 0, y = 0}

  if position.x < origin.x then
    vector.x = (origin.x - radius) - position.x
  elseif position.x > origin.x then
    vector.x = (origin.x + radius) - position.x
  end

  if position.y < origin.y then
    vector.y = (origin.y - radius) - position.y
  elseif position.y > origin.y then
    vector.y = (origin.y + radius) - position.y
  end

  if math.abs(vector.x) < math.abs(vector.y) then
    vector.y = 0
  else
    vector.x = 0
  end

  local new_position = {x = position.x + vector.x, y = position.y + vector.y}
  local vehicle = player.vehicle
  if vehicle then
    if not vehicle.teleport(new_position) then
      player.driving = false
    end
    vehicle.orientation = vehicle.orientation + 0.5
  else
    player.teleport(new_position)
  end

  if script_data.check_base_exclusion then
    local time_left = math.ceil((script_data.round_start_tick + (script_data.config.game_config.base_exclusion_time * 60 * 60) - game.tick) / 3600)
    player.print({"base-exclusion-teleport", time_left})
  else
    player.print({"protected-base-area"})
  end

end

local should_start = function()
  if script_data.ready_start_tick and script_data.ready_start_tick <= game.tick then
    return true
  end

  if not script_data.team_won then return false end
  local time = script_data.config.game_config.auto_new_round_time
  if not (time > 0) then return false end
  if game.tick < (script_data.config.game_config.auto_new_round_time * 60 * 60) + script_data.team_won then return false end
  return true
end

function check_restart_round()
  if not should_start() then return end
  end_round()
  start_round()
end

function team_won(name)
  script_data.team_won = game.tick
  if script_data.config.game_config.auto_new_round_time > 0 then
    game.print({"team-won-auto", name, script_data.config.game_config.auto_new_round_time})
  else
    game.print({"team-won", name})
  end
  script.raise_event(events.on_team_won, {name = name})
end

function offset_respawn_position(player)
  --This is to help the spawn camping situations.
  if not (player and player.valid and player.character) then return end
  local surface = player.surface
  local origin = player.force.get_spawn_position(surface)
  local radius = get_starting_area_radius(true) - 32
  if not (radius > 0) then return end
  local random_position = {origin.x + math.random(-radius, radius), origin.y + math.random(-radius, radius)}
  local position = surface.find_non_colliding_position(player.character.name, random_position, 32, 1)
  if not position then return end
  player.teleport(position)
end

function recursive_data_check(new_data, old_data)
  for k, data in pairs (new_data) do
    if old_data[k] == nil then
      old_data[k] = data
    elseif type(data) ~= type(old_data[k]) then
      old_data[k] = data
    elseif type(data) == "table" then
      recursive_data_check(new_data[k], old_data[k])
    end
  end
end

function check_cursor_for_disabled_items(event)
  if not script_data.config.disabled_items then return end
  local player = game.players[event.player_index]
  if not (player and player.valid) then return end
  local stack = player.cursor_stack
  if (stack and stack.valid_for_read) then
    if script_data.config.disabled_items[stack.name] then
      stack.clear()
    end
  end
end

function recipe_picker_elem_update(player)
  if not (player and player.valid) then return end
  local production_score_frame = script_data.elements.production_score_frame[player.index]
  if not (production_score_frame and production_score_frame.valid) then return end

  local recipe_frame = script_data.elements.recipe_frame[player.index]
  if recipe_frame and recipe_frame.valid then
    deregister_gui(recipe_frame)
    recipe_frame.destroy()
    script_data.elements.recipe_frame[player.index] = nil
  end

  local elem_value = script_data.selected_recipe[player.index]
  local elem_button = script_data.elements.recipe_button[player.index]
  if (elem_button and elem_button.valid) then
    elem_button.elem_value = elem_value
  end

  if not elem_value then return end

  local recipe = player.force.recipes[elem_value]
  local recipe_frame = production_score_frame.add{type = "frame", direction = "vertical", style = "inside_shallow_frame"}
  script_data.elements.recipe_frame[player.index] = recipe_frame
  local title_flow = recipe_frame.add{type = "flow"}
  title_flow.style.horizontal_align = "center"
  title_flow.style.horizontally_stretchable = true
  title_flow.add{type = "label", caption = recipe.localised_name, style = "caption_label"}
  local table = recipe_frame.add{type = "table", column_count = 2}
  table.draw_horizontal_line_after_headers = true
  table.draw_vertical_lines = true
  table.style.horizontal_spacing = 16
  table.style.vertical_spacing = 2
  table.style.left_padding = 4
  table.style.right_padding = 4
  table.style.top_padding = 4
  table.style.bottom_padding = 4
  table.style.column_alignments[1] = "center"
  table.style.column_alignments[2] = "center"
  table.add{type = "label", caption = {"ingredients"}, style = "bold_label"}
  table.add{type = "label", caption = {"products"}, style = "bold_label"}
  local ingredients = recipe.ingredients
  local products = recipe.products
  local prices = script_data.price_list
  local cost = 0
  local gain = 0
  local prototypes =
  {
    fluid = game.fluid_prototypes,
    item = game.item_prototypes
  }
  for k = 1, math.max(#ingredients, #products) do
    local ingredient = ingredients[k]
    local flow = table.add{type = "flow", direction = "horizontal"}
    if k == 1 then
      flow.style.top_padding = 8
    end
    flow.style.vertical_align = "center"
    if ingredient then
      local ingredient_price = prices[ingredient.name] or 0
      local calculator_button = flow.add
      {
        type = "sprite-button",
        sprite = ingredient.type.."/"..ingredient.name,
        number = ingredient.amount,
        style = "transparent_slot",
        tooltip = {"", "1 ", prototypes[ingredient.type][ingredient.name].localised_name, " = ", util.format_number(math.floor(ingredient_price * 100) / 100)}
      }
      register_gui_action(calculator_button, {type = "calculator_button_press", elem_type = ingredient.type, elem_name = ingredient.name})
      local price = ingredient.amount * ingredient_price or 0
      add_pusher(flow)
      flow.add{type = "label", caption = util.format_number(math.floor(price * 100) / 100)}
      cost = cost + price
    end
    local product = products[k]
    flow = table.add{type = "flow", direction = "horizontal"}
    if k == 1 then
      flow.style.top_padding = 8
    end
    flow.style.vertical_align = "center"
    if product then
      local amount = util.product_amount(product)
      local product_price = prices[product.name] or 0
      local calculator_button = flow.add
      {
        type = "sprite-button",
        sprite = product.type.."/"..product.name,
        number = amount,
        style = "transparent_slot",
        tooltip = {"", "1 ", prototypes[product.type][product.name].localised_name, " = ", util.format_number(math.floor(product_price * 100) / 100)},
        show_percent_for_small_numbers = true
      }
      register_gui_action(calculator_button, {type = "calculator_button_press", elem_type = product.type, elem_name = product.name})
      add_pusher(flow)
      local price = amount * product_price or 0
      flow.add{type = "label", caption = util.format_number(math.floor(price * 100) / 100)}
      gain = gain + price
    end
  end
  local line = table.add{type = "table", column_count = 1}
  line.draw_horizontal_lines = true
  add_pusher(line)
  add_pusher(line)
  line.style.top_padding = 8
  line.style.bottom_padding = 4
  local line = table.add{type = "table", column_count = 1}
  line.draw_horizontal_lines = true
  add_pusher(line)
  add_pusher(line)
  line.style.top_padding = 8
  line.style.bottom_padding = 4
  local cost_flow = table.add{type = "flow"}
  cost_flow.add{type = "label", caption = {"", {"cost"}, {"colon"}}}
  add_pusher(cost_flow)
  cost_flow.add{type = "label", caption = util.format_number(math.floor(cost * 100) / 100)}
  local gain_flow = table.add{type = "flow"}
  gain_flow.add{type = "label", caption = {"", {"gain"}, {"colon"}}}
  add_pusher(gain_flow)
  gain_flow.add{type = "label", caption = util.format_number(math.floor(gain * 100) / 100)}
  table.add{type = "flow"}
  local total_flow = table.add{type = "flow"}
  total_flow.add{type = "label", caption = {"", {"total"}, {"colon"}}, style = "bold_label"}
  add_pusher(total_flow)
  local total = total_flow.add{type = "label", caption = util.format_number(math.floor((gain-cost) * 100) / 100), style = "bold_label"}
  if cost > gain then
    total.style.font_color = {r = 1, g = 0.3, b = 0.3}
  end

end

function add_pusher(gui)
  local pusher = gui.add{type = "flow"}
  pusher.style.horizontally_stretchable = true
end

function check_on_built_protection(event)
  if not script_data.config.game_config.enemy_building_restriction then return end
  local entity = event.created_entity
  local player = game.players[event.player_index]
  if not (entity and entity.valid and player and player.valid) then return end
  local force = entity.force
  local name = get_chunk_map_position(entity.position)
  if not name then return end
  if force.name == name then return end
  local other_force = game.forces[name]
  if not other_force then return end
  if other_force.get_friend(force) then return end
  if not player.mine_entity(entity, true) then
    entity.destroy()
  end
  player.print({"enemy-building-restriction"})
end

function check_defcon()
  if not script_data.config.team_config.defcon_mode then return end
  local defcon_tick = script_data.last_defcon_tick
  if not defcon_tick then
    defcon_research()
    return
  end
  local current_tick = game.tick
  local duration = math.max(60, (script_data.config.team_config.defcon_timer * 60 * 60))
  local tick_of_defcon = defcon_tick + duration
  local progress = math.max(0, math.min(1, 1 - (tick_of_defcon - current_tick) / duration))
  local tech = script_data.next_defcon_tech
  if tech and tech.valid then
    for k, team in pairs (script_data.config.teams) do
      local force = game.forces[team.name]
      if force then
        if (not force.current_research) or (force.current_research.name ~= tech.name) then
          force.cancel_current_research()
          force.add_research(tech)
        end
        if force.current_research then
          --If they have labs, it might research it between the defcon updates...
          force.research_progress = progress
        end
      end
    end
  end
  if current_tick >= tick_of_defcon then
    defcon_research()
  end
end

recursive_technology_prerequisite = function(tech)
  for name, prerequisite in pairs (tech.prerequisites) do
    if not prerequisite.researched then
      return recursive_technology_prerequisite(prerequisite)
    end
  end
  return tech
end

function defcon_research()
  script_data.last_defcon_tick = game.tick
  local tech = script_data.next_defcon_tech
  if tech and tech.valid then
    for k, team in pairs (script_data.config.teams) do
      local force = game.forces[team.name]
      if force then
        local tech = force.technologies[tech.name]
        if tech then
          tech.researched = true
        end
      end
    end
    local sound = "utility/research_completed"
    if game.is_valid_sound_path(sound) then
      game.play_sound({path = sound})
    end
    game.print({"defcon-unlock", tech.localised_name}, {r = 1, g = 0.5, b = 0.5})
  end

  local force
  for k, team in pairs (script_data.config.teams) do
    force = game.forces[team.name]
    if force and force.valid then
      break
    end
  end
  if not force then return end
  local available_techs = {}
  for name, tech in pairs (force.technologies) do
    if tech.enabled and tech.researched == false then
      insert(available_techs, tech)
    end
  end
  if #available_techs == 0 then return end
  local random_tech = available_techs[math.random(#available_techs)]
  if not random_tech then return end
  random_tech = recursive_technology_prerequisite(random_tech)
  script_data.next_defcon_tech = game.technology_prototypes[random_tech.name]
  for k, team in pairs (script_data.config.teams) do
    local force = game.forces[team.name]
    if force then
      force.add_research(script_data.next_defcon_tech)
    end
  end
end

function check_neutral_chests(event)
  if not script_data.config.game_config.neutral_chests then return end
  local entity = event.created_entity
  if not (entity and entity.valid) then return end
  if entity.type == "container" then
    entity.force = "neutral"
  end
end

function on_calculator_button_press(event, param)
  local gui = event.element
  if not (gui and gui.valid) then return end
  local player = game.players[event.player_index]
  if not (player and player.valid) then return end
  local type = param.elem_type
  local elem_name = param.elem_name
  local items = game.item_prototypes
  local fluids = game.fluid_prototypes
  local recipes = game.recipe_prototypes
  if type == "item" then
    if not items[elem_name] then return end
  elseif type == "fluid" then
    if not fluids[elem_name] then return end
  else
    return
  end
  local selected = script_data.selected_recipe[player.index]
  local candidates = {}
  for name, recipe in pairs (recipes) do
    for k, product in pairs (recipe.products) do
      if product.type == type and product.name == elem_name then
        insert(candidates, name)
      end
    end
  end
  if #candidates == 0 then return end
  local index = 0
  for k, name in pairs (candidates) do
    if name == selected then
      index = k
      break
    end
  end
  local recipe_name = candidates[index + 1] or candidates[1]
  if not recipe_name then return end
  script_data.selected_recipe[player.index] = recipe_name
  recipe_picker_elem_update(player)
end

function generic_gui_event(event)
  local gui = event.element
  if not (gui and gui.valid) then return end
  local player_gui_actions = script_data.gui_actions[gui.player_index]
  if not player_gui_actions then return end
  local action = player_gui_actions[gui.index]
  if action then
    gui_functions[action.type](event, action)
    return true
  end
end

function update_timers()
  local caption = {"time_left", get_time_left()}
  for k, timer in pairs (script_data.timers) do
    if timer.valid then
      timer.caption = caption
    end
  end
end

local on_rocket_launched = function(event)
  if not script_data.config.victory.space_race.active then return end
  local sent = get_rocket_scores()
  local name = event.rocket.force.name
  sent[name] = sent[name] + 1
  check_update_space_race_score()
end

local check_last_silo_standing_victory = function(event)
  if script_data.team_won then return end
  if not script_data.config.victory.last_silo_standing.active then return end
  local silo = event.entity
  if not (silo and silo.valid and silo.name == (script_data.config.prototypes.silo or "") ) then
    return
  end
  local killing_force = event.force
  local force = silo.force
  if not script_data.silos then return end
  script_data.silos[force.name] = nil
  if killing_force then
    game.print({"silo-destroyed", force.name, killing_force.name})
  else
    game.print({"silo-destroyed", force.name, {"neutral"}})
  end
  script.raise_event(events.on_team_lost, {name = force.name})

  for k, player in pairs (force.players) do
    local character = player.character
    if character then
      player.character = nil
      character.die()
    end
    player.force = "neutral"
    player.set_controller{type = defines.controllers.spectator}
  end

  game.merge_forces(force, "neutral")

  local index = 0
  local winner_name = {"none"}
  for name, listed_silo in pairs (script_data.silos) do
    if (listed_silo and listed_silo.valid) then
      index = index + 1
      winner_name = name
    end
  end

  if index == 1  then
    team_won(winner_name)
    return
  end

  if index == 0 then
    -- All silos are destroyed, which can happen with only 1 team
    -- So we just set the victory tick manually.
    script_data.team_won = game.tick
    return
  end

end

local update_kill_score = function(event)
  if not script_data.config.victory.kill_score.active then return end
  local entity = event.entity
  if not (entity and entity.valid) then return end

  local cause_force = event.force
  if not (cause_force and cause_force.valid) then return end

  local scores = get_kill_scores()
  local prices = script_data.entity_prices
  if not prices then
    prices = kill_score.generate_entity_prices()
    script_data.entity_prices = prices
  end

  local force_name = cause_force.name
  scores[force_name] = scores[force_name] + (prices[entity.name] or 0)

end

local on_entity_died = function(event)
  update_kill_score(event)
  check_last_silo_standing_victory(event)
end

local on_player_joined_game = function(event)
  local player = game.players[event.player_index]
  if not (player and player.valid) then return end
  player.spectator = true
  init_player_gui(player)

  if not script_data.setup_finished then
    refresh_config(player.index)
  end

  if player.force.name ~= "player" then
    --If they are not on the player force, they have already picked a team this round.
    check_force_protection(player.force)
    for k, other_player in pairs (game.connected_players) do
      update_team_list_frame(other_player)
    end
    return
  end
  local character = player.character
  player.character = nil
  if character then character.destroy() end
  player.set_controller{type = defines.controllers.spectator}
  player.teleport({0, 1000}, get_lobby_surface())
end

local on_player_left_game = function(event)
  local player = game.players[event.player_index]

  if script_data.setup_finished then
    for k, player in pairs (game.connected_players) do
      local gui = player.gui.center
      choose_joining_gui(player)
      choose_joining_gui(player)
      update_team_list_frame(player)
    end
    check_force_protection(force)
  else
    refresh_config(event.player_index)
  end

  destroy_player_gui(player)
end

local on_tick = function(event)
  if script_data.setup_finished == false then
    check_starting_area_chunks_are_generated()
    finish_setup()
  end
end

local on_player_respawned = function(event)
  local player = game.get_player(event.player_index)
  if not (player and player.valid) then return end
  if script_data.setup_finished == true then
    config.give_equipment(player)
    offset_respawn_position(player)
    balance.apply_character_modifiers(player)
  else
    if player.character then
      player.character.destroy()
    end
  end
end

local on_research_finished = function(event)
  check_technology_for_disabled_items(event)
end

local on_player_cursor_stack_changed = function(event)
  check_cursor_for_disabled_items(event)
end

local on_built_entity = function(event)
  check_on_built_protection(event)
  check_neutral_chests(event)
end

local on_robot_built_entity = function(event)
  check_neutral_chests(event)
end

local on_research_started = function(event)
  if script_data.config.team_config.defcon_mode then
    local tech = script_data.next_defcon_tech
    local research = event.research
    local force = research.force
    if not is_ignored_force(force.name) then
      if tech and tech.valid and research.name ~= tech.name then
        force.cancel_current_research()
        force.add_research(tech)
      end
    end
  end
end

local on_player_event_refresh_gui = function(event)
  init_player_gui(game.get_player(event.player_index))
end

local on_forces_merged = function (event)
  create_exclusion_map()
end

local on_player_changed_position = function(event)
  local player = game.players[event.player_index]
  check_player_base_exclusion(player)
  check_player_no_rush(player)
end

local check_spectator_chart = function()
  local force = game.forces.neutral
  if not (force and force.valid) then return end
  local surface = script_data.surface
  if not (surface and surface.valid) then return end
  force.chart_all(script_data.surface)
end

function destroy_016_player_guis()
  for k, player in pairs (game.players) do
    local button_flow = mod_gui.get_button_flow(player)
    for k, name in pairs (
      {
        "objective_button", "diplomacy_button", "admin_button",
        "silo_gui_sprite_button", "production_score_button", "oil_harvest_button",
        "space_race_button", "spectator_join_team_button", "list_teams_button"
      }) do
      if button_flow[name] then
        button_flow[name].destroy()
      end
    end
    local frame_flow = mod_gui.get_frame_flow(player)
    for k, name in pairs (
      {
        "objective_frame", "admin_button", "admin_frame",
        "silo_gui_frame", "production_score_frame", "oil_harvest_frame",
        "space_race_frame", "team_list"
      }) do
      if frame_flow[name] then
        frame_flow[name].destroy()
      end
    end
    local center_gui = player.gui.center
    for k, name in pairs ({"diplomacy_frame", "progress_bar", "random_join_frame", "pick_join_frame", "auto_assign_frame"}) do
      if center_gui[name] then
        center_gui[name].destroy()
      end
    end
  end
end

local pvp = {}

pvp.events =
{
  [defines.events.on_built_entity] = on_built_entity,
  [defines.events.on_robot_built_entity] = on_robot_built_entity,
  [defines.events.on_chunk_generated] = on_chunk_generated,
  [defines.events.on_entity_died] = on_entity_died,
  [defines.events.on_forces_merged] = on_forces_merged,

  [defines.events.on_gui_checked_state_changed] = generic_gui_event,
  [defines.events.on_gui_click] = generic_gui_event,
  [defines.events.on_gui_closed] = generic_gui_event,
  [defines.events.on_gui_elem_changed] = generic_gui_event,
  [defines.events.on_gui_selection_state_changed] = generic_gui_event,
  [defines.events.on_gui_text_changed] = generic_gui_event,

  [defines.events.on_player_joined_game] = on_player_joined_game,
  [defines.events.on_player_left_game] = on_player_left_game,
  [defines.events.on_player_respawned] = on_player_respawned,
  [defines.events.on_player_changed_position] = on_player_changed_position,
  [defines.events.on_player_cursor_stack_changed] = on_player_cursor_stack_changed,

  [defines.events.on_player_demoted] = on_player_event_refresh_gui,
  [defines.events.on_player_display_resolution_changed] = on_player_event_refresh_gui,
  [defines.events.on_player_display_scale_changed] = on_player_event_refresh_gui,
  [defines.events.on_player_promoted] = on_player_event_refresh_gui,
  [defines.events.on_player_changed_force] = on_player_event_refresh_gui,

  [defines.events.on_research_finished] = on_research_finished,
  [defines.events.on_research_started] = on_research_started,

  [defines.events.on_rocket_launched] = on_rocket_launched,
  [defines.events.on_tick] = on_tick
}

pvp.add_remote_interface = function()
  remote.add_interface("pvp",
  {
    get_event_name = function(name)
      return events[name]
    end,
    get_events = function()
      return events
    end,
    get_teams = function()
      return script_data.config.teams
    end,
    get_config = function()
      return script_data.config
    end,
    set_config = function(array)
      log("PvP global config set by remote call - Can expect script errors after this point.")
      for k, v in pairs (array) do
        script_data.config[k] = v
      end
    end
  })
end

pvp.on_nth_tick =
{
  [60] = function(event)
    if script_data.setup_finished == true then
      check_no_rush()
      check_update_production_score()
      check_update_oil_harvest_score()
      check_update_space_race_score()
      check_update_kill_score()
      check_base_exclusion()
      check_defcon()
      update_timers()
    end
    check_restart_round()
  end,
  [300] = function(event)
    if script_data.setup_finished == true then
      check_player_color()
      check_spectator_chart()
    end
  end
}

pvp.on_load = function()
  script_data = global.pvp or script_data
  balance.script_data = script_data
  config.script_data = script_data
end

pvp.on_init = function()
  if global.pvp then
    --Init was already run, just do the on_load.
    pvp.on_load()
    return
  end
  script_data.config = config.get_config()
  global.pvp = script_data
  balance.script_data = script_data
  config.script_data = script_data
  balance.init()
  script_data.config.game_config.seed = game.surfaces[1].map_gen_settings.seed
  for k, force in pairs (game.forces) do
    force.disable_all_prototypes()
    force.disable_research()
  end
end

pvp.on_configuration_changed = function(data)
  if not global.pvp and global.surface and global.teams then
    --Was made in 0.16, do some basic data migration
    local pvp = global
    global = {pvp = pvp}
    recursive_data_check(script_data, global.pvp)
    script_data = global.pvp
    script_data.config = config.get_config()
    local config = script_data.config
    config.teams = script_data.teams
    script_data.teams = nil
    destroy_016_player_guis()
    balance.script_data = script_data
    config.script_data = script_data
  end
  recursive_data_check(config.get_config(), script_data.config)

  script_data.random = script_data.random or game.create_random_generator()
  update_teams_names()

  --[[
    if game.forces.spectator then
      game.merge_forces("spectator", "neutral")
    end
  ]]

  script_data.elements.team_tab = script_data.elements.team_tab or {}
  script_data.elements.game_tab = script_data.elements.game_tab or {}

end

return pvp
