{% from "macros.lua" import dict_to_lua %}
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
require "lib"
require "util"

FREE_SAMPLES = {{ free_samples }}
SLOT_NAME = "{{ slot_name }}"
SEED_NAME = "{{ seed_name }}"
FREE_SAMPLE_BLACKLIST = {{ dict_to_lua(free_sample_blacklist) }}
TRAP_EVO_FACTOR = {{ evolution_trap_increase }} / 100
MAX_SCIENCE_PACK = {{ max_science_pack }}
GOAL = {{ goal }}
ARCHIPELAGO_DEATH_LINK_SETTING = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}"
ENERGY_INCREMENT = {{ energy_link * 10000000 }}
ENERGY_LINK_EFFICIENCY = 0.75

if settings.global[ARCHIPELAGO_DEATH_LINK_SETTING].value then
    DEATH_LINK = 1
else
    DEATH_LINK = 0
end

CURRENTLY_DEATH_LOCK = 0

{% if chunk_shuffle %}
LAST_POSITIONS = {}
GENERATOR = nil
NORTH = 1
EAST = 2
SOUTH = 3
WEST = 4
ER_COLOR = {1, 1, 1, 0.2}
ER_SEED = {{ random.randint(4294967295, 2*4294967295)}}
CURRENTLY_MOVING = false
ER_FRAMES = {}
CHUNK_OFFSET = {
[NORTH] = {0, 1},
[EAST] = {1, 0},
[SOUTH] = {0, -1},
[WEST] = {-1, 0}
}


function on_player_changed_position(event)
    if CURRENTLY_MOVING == true then
        return
    end
    local player_id = event.player_index
    local player = game.get_player(player_id)
    local character = player.character -- can be nil, such as spectators

    if character == nil then
        return
    end
    local last_position = LAST_POSITIONS[player_id]
    if last_position == nil then
        LAST_POSITIONS[player_id] = character.position
        return
    end

    last_x_chunk = math.floor(last_position.x / 32)
    current_x_chunk = math.floor(character.position.x / 32)
    last_y_chunk = math.floor(last_position.y / 32)
    current_y_chunk = math.floor(character.position.y / 32)
    if (ER_FRAMES[player_id] ~= nil and rendering.is_valid(ER_FRAMES[player_id])) then
        rendering.destroy(ER_FRAMES[player_id])
    end
    ER_FRAMES[player_id] = rendering.draw_rectangle{
        color=ER_COLOR, width=1, filled=false, left_top = {current_x_chunk*32, current_y_chunk*32},
        right_bottom={current_x_chunk*32+32, current_y_chunk*32+32}, players={player}, time_to_live=60,
        draw_on_ground= true, only_in_alt_mode = true, surface=character.surface}
    if current_x_chunk == last_x_chunk and current_y_chunk == last_y_chunk then -- nothing needs doing
        return
    end
    if ((last_position.x - character.position.x) ^ 2 + (last_position.y - character.position.y) ^ 2) > 4000 then
        -- distance too high, death or other teleport took place
        LAST_POSITIONS[player_id] = character.position
        return
    end
    -- we'll need a deterministic random state
    if GENERATOR == nil or not GENERATOR.valid then
        GENERATOR = game.create_random_generator()
    end

    -- sufficiently random pattern
    GENERATOR.re_seed((ER_SEED + (last_x_chunk * 1730000000) + (last_y_chunk * 97000)) % 4294967295)
    -- we now need all 4 exit directions deterministically shuffled to the 4 outgoing directions.
    local exit_table = {
    [1] = 1,
    [2] = 2,
    [3] = 3,
    [4] = 4
    }
    exit_table = fisher_yates_shuffle(exit_table)
    if current_x_chunk > last_x_chunk then -- going right/east
        outbound_direction = EAST
    elseif current_x_chunk < last_x_chunk then -- going left/west
        outbound_direction = WEST
    end

    if current_y_chunk > last_y_chunk then -- going down/south
        outbound_direction = SOUTH
    elseif current_y_chunk < last_y_chunk then -- going up/north
        outbound_direction = NORTH
    end
    local target_direction = exit_table[outbound_direction]

    local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16,
                             (CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16}
    target_position = character.surface.find_non_colliding_position(character.prototype.name,
                                                                    target_position, 32, 0.5)
    if target_position ~= nil then
        rendering.draw_circle{color = ER_COLOR, radius = 1, filled = true,
                              target = {character.position.x, character.position.y}, surface = character.surface,
                              time_to_live = 300, draw_on_ground = true}
        rendering.draw_line{color = ER_COLOR, width = 3, gap_length = 0.5, dash_length = 0.5,
                            from = {character.position.x, character.position.y}, to = target_position,
                            surface = character.surface,
                            time_to_live = 300, draw_on_ground = true}
        CURRENTLY_MOVING = true -- prevent recursive event
        character.teleport(target_position)
        CURRENTLY_MOVING = false
    end
    LAST_POSITIONS[player_id] = character.position
end

function fisher_yates_shuffle(tbl)
    for i = #tbl, 2, -1 do
        local j = GENERATOR(i)
        tbl[i], tbl[j] = tbl[j], tbl[i]
    end
    return tbl
end

script.on_event(defines.events.on_player_changed_position, on_player_changed_position)
{% endif %}
-- Handle the pathfinding result of teleport traps
script.on_event(defines.events.on_script_path_request_finished, handle_teleport_attempt)

function count_energy_bridges()
    local count = 0
    for i, bridge in pairs(storage.energy_link_bridges) do
        if validate_energy_link_bridge(i, bridge) then
            count = count + 1 + (bridge.quality.level * 0.3)
        end
    end
    return count
end

function get_energy_increment(bridge)
    return ENERGY_INCREMENT + (ENERGY_INCREMENT * 0.3 * bridge.quality.level)
end

function on_check_energy_link(event)
    --- assuming 1 MJ increment and 5MJ battery:
    --- first 2 MJ request fill, last 2 MJ push energy, middle 1 MJ does nothing
    if event.tick % 60 == 30 then
        local force = "player"
        local bridges = storage.energy_link_bridges
        local bridgecount = count_energy_bridges()
        storage.forcedata[force].energy_bridges = bridgecount
        if storage.forcedata[force].energy == nil then
            storage.forcedata[force].energy = 0
        end
        if storage.forcedata[force].energy < ENERGY_INCREMENT * bridgecount * 5 then
            for i, bridge in pairs(bridges) do
                if validate_energy_link_bridge(i, bridge) then
                    energy_increment = get_energy_increment(bridge)
                    if bridge.energy > energy_increment*3 then
                        storage.forcedata[force].energy = storage.forcedata[force].energy + (energy_increment * ENERGY_LINK_EFFICIENCY)
                        bridge.energy = bridge.energy - energy_increment
                    end
                end
            end
        end
        for i, bridge in pairs(bridges) do
            if validate_energy_link_bridge(i, bridge) then
                energy_increment = get_energy_increment(bridge)
                if storage.forcedata[force].energy < energy_increment and bridge.quality.level == 0 then
                    break
                end
                if bridge.energy < energy_increment*2 and storage.forcedata[force].energy > energy_increment then
                    storage.forcedata[force].energy = storage.forcedata[force].energy - energy_increment
                    bridge.energy = bridge.energy + energy_increment
                end
            end
        end
    end
end
function string_starts_with(str, start)
    return str:sub(1, #start) == start
end
function validate_energy_link_bridge(unit_number, entity)
    if not entity then
        if storage.energy_link_bridges[unit_number] == nil then return false end
        storage.energy_link_bridges[unit_number] = nil
        return false
    end
    if not entity.valid then
        if storage.energy_link_bridges[unit_number] == nil then return false end
        storage.energy_link_bridges[unit_number] = nil
        return false
    end
    return true
end
function on_energy_bridge_constructed(entity)
    if entity and entity.valid then
        if string_starts_with(entity.prototype.name, "ap-energy-bridge") then
            storage.energy_link_bridges[entity.unit_number] = entity
        end
    end
end
function on_energy_bridge_removed(entity)
    if string_starts_with(entity.prototype.name, "ap-energy-bridge") then
        if storage.energy_link_bridges[entity.unit_number] == nil then return end
        storage.energy_link_bridges[entity.unit_number] = nil
    end
end
if (ENERGY_INCREMENT) then
    script.on_event(defines.events.on_tick, on_check_energy_link)

    script.on_event({defines.events.on_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end)
    script.on_event({defines.events.on_robot_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end)
    script.on_event({defines.events.on_entity_cloned}, function(event) on_energy_bridge_constructed(event.destination) end)

    script.on_event({defines.events.script_raised_revive}, function(event) on_energy_bridge_constructed(event.entity) end)
    script.on_event({defines.events.script_raised_built}, function(event) on_energy_bridge_constructed(event.entity) end)

    script.on_event({defines.events.on_entity_died}, function(event) on_energy_bridge_removed(event.entity) end)
    script.on_event({defines.events.on_player_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end)
    script.on_event({defines.events.on_robot_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end)
end

{% if not imported_blueprints -%}
function set_permissions()
    local group = game.permissions.get_group("Default")
    group.set_allows_action(defines.input_action.open_blueprint_library_gui, false)
    group.set_allows_action(defines.input_action.import_blueprint, false)
    group.set_allows_action(defines.input_action.import_blueprint_string, false)
    group.set_allows_action(defines.input_action.import_blueprints_filtered, false)
end
{%- endif %}


function check_spawn_silo(force)
    if force.players and #force.players > 0 and force.get_entity_count("rocket-silo") < 1 then
        local surface = game.get_surface(1)
        local spawn_position = force.get_spawn_position(surface)
        spawn_entity(surface, force, "rocket-silo", spawn_position.x, spawn_position.y, 80, true, true)
        spawn_entity(surface, force, "cargo-landing-pad", spawn_position.x, spawn_position.y, 80, true, true)
    end
end

function check_despawn_silo(force)
    if not force.players or #force.players < 1 then
        if force.get_entity_count("rocket-silo") > 0 then
            local surface = game.get_surface(1)
            local spawn_position = force.get_spawn_position(surface)
            local x1 = spawn_position.x - 41
            local x2 = spawn_position.x + 41
            local y1 = spawn_position.y - 41
            local y2 = spawn_position.y + 41
            local silos = surface.find_entities_filtered{area = { {x1, y1}, {x2, y2} },
                                                         name = "rocket-silo",
                                                         force = force}
            for i, silo in ipairs(silos) do
                silo.destructible = true
                silo.destroy()
            end
        end
        if force.get_entity_count("cargo-landing-pad") > 0 then
            local surface = game.get_surface(1)
            local spawn_position = force.get_spawn_position(surface)
            local x1 = spawn_position.x - 41
            local x2 = spawn_position.x + 41
            local y1 = spawn_position.y - 41
            local y2 = spawn_position.y + 41
            local pads = surface.find_entities_filtered{area = { {x1, y1}, {x2, y2} },
                                                        name = "cargo-landing-pad",
                                                        force = force}
            for i, pad in ipairs(pads) do
                pad.destructible = true
                pad.destroy()
            end
        end
    end
end


-- Initialize force data, either from it being created or already being part of the game when the mod was added.
function on_force_created(event)
    local force = event.force
    if type(event.force) == "string" then  -- should be of type LuaForce
        force = game.forces[force]
    end
    local data = {}
    data['earned_samples'] = {{ dict_to_lua(starting_items) }}
    data["victory"] = 0
    data["death_link_tick"] = 0
    data["energy"] = 0
    data["energy_bridges"] = 0
    storage.forcedata[event.force] = data
{%- if silo == 2 %}
    check_spawn_silo(force)
{%- endif %}
{%- for tech_name in removed_technologies %}
    force.technologies["{{ tech_name }}"].researched = true
{%- endfor %}
end
script.on_event(defines.events.on_force_created, on_force_created)

-- Destroy force data.  This doesn't appear to be currently possible with the Factorio API, but here for completeness.
function on_force_destroyed(event)
{%- if silo == 2 %}
    check_despawn_silo(event.force)
{%- endif %}
    storage.forcedata[event.force.name] = nil
end

function on_runtime_mod_setting_changed(event)
    local force
    if event.player_index == nil then
        force = game.forces.player
    else
        force = game.players[event.player_index].force
    end

    if event.setting == ARCHIPELAGO_DEATH_LINK_SETTING then
        if settings.global[ARCHIPELAGO_DEATH_LINK_SETTING].value then
            DEATH_LINK = 1
        else
            DEATH_LINK = 0
        end
        if force ~= nil then
            dumpInfo(force)
        end
    end
end
script.on_event(defines.events.on_runtime_mod_setting_changed, on_runtime_mod_setting_changed)

-- Initialize player data, either from them joining the game or them already being part of the game when the mod was
-- added.`
function on_player_created(event)
    local player = game.players[event.player_index]
    -- FIXME: This (probably) fires before any other mod has a chance to change the player's force
    -- For now, they will (probably) always be on the 'player' force when this event fires.
    local data = {}
    data['pending_samples'] = table.deepcopy(storage.forcedata[player.force.name]['earned_samples'])
    storage.playerdata[player.index] = data
    update_player(player.index)  -- Attempt to send pending free samples, if relevant.
{%- if silo == 2 %}
    check_spawn_silo(game.players[event.player_index].force)
{%- endif %}
    dumpInfo(player.force)
end
script.on_event(defines.events.on_player_created, on_player_created)

-- Create/destroy silo for force if player switched force
function on_player_changed_force(event)
{%- if silo == 2 %}
    check_despawn_silo(event.force)
    check_spawn_silo(game.players[event.player_index].force)
{%- endif %}
end
script.on_event(defines.events.on_player_changed_force, on_player_changed_force)

function on_player_removed(event)
    storage.playerdata[event.player_index] = nil
end
script.on_event(defines.events.on_player_removed, on_player_removed)

function on_rocket_launched(event)
    if event.rocket and event.rocket.valid and storage.forcedata[event.rocket.force.name]['victory'] == 0 then
        satellite_count = 0
        cargo_pod = event.rocket.cargo_pod
        if cargo_pod then
            satellite_count = cargo_pod.get_item_count("satellite")
        end
        if satellite_count > 0 or GOAL == 0 then
            storage.forcedata[event.rocket.force.name]['victory'] = 1
            dumpInfo(event.rocket.force)
            game.set_game_state
            {
                game_finished = true,
                player_won = true,
                can_continue = true,
                victorious_force = event.rocket.force
            }
        end
    end
end
script.on_event(defines.events.on_rocket_launched, on_rocket_launched)

-- Updates a player, attempting to send them any pending samples (if relevant)
function update_player(index)
    local player = game.players[index]
    if not player or not player.valid then     -- Do nothing if we reference an invalid player somehow
        return
    end
    local character = player.character or player.cutscene_character
    if not character or not character.valid then
        return
    end
    local data = storage.playerdata[index]
    local samples = data['pending_samples']
    local sent
    --player.print(serpent.block(data['pending_samples']))
    local stack = {}

    for name, count in pairs(samples) do
        stack.name = name
        stack.count = count
        if script.active_mods["quality"] then
            stack.quality = "{{ free_sample_quality_name }}"
        end
        if prototypes.item[name] then
            if character.can_insert(stack) then
                sent = character.insert(stack)
            else
                sent = 0
            end
            if sent > 0 then
                player.print("Received " .. sent .. "x [item=" .. name .. ",quality={{ free_sample_quality_name }}]")
                data.suppress_full_inventory_message = false
            end
            if sent ~= count then               -- Couldn't full send.
                if not data.suppress_full_inventory_message then
                    player.print("Additional items will be sent when inventory space is available.", {r=1, g=1, b=0.25})
                end
                data.suppress_full_inventory_message = true -- Avoid spamming them with repeated full inventory messages.
                samples[name] = count - sent    -- Buffer the remaining items
                break                           -- Stop trying to send other things
            else
                samples[name] = nil             -- Remove from the list
            end
        else
            player.print("Unable to receive " .. count .. "x [item=" .. name .. "] as this item does not exist.")
            samples[name] = nil
        end
    end

end

-- Update players upon them connecting, since updates while they're offline are suppressed.
script.on_event(defines.events.on_player_joined_game, function(event) update_player(event.player_index) end)

function update_player_event(event)
    update_player(event.player_index)
end

script.on_event(defines.events.on_player_main_inventory_changed, update_player_event)

-- Update players when the cutscene is cancelled or finished.  (needed for skins_factored)
script.on_event(defines.events.on_cutscene_cancelled, update_player_event)
script.on_event(defines.events.on_cutscene_finished, update_player_event)

function add_samples(force, name, count)
    local function add_to_table(t)
        if count <= 0 then
            -- Fixes a bug with single craft, if a recipe gives 0 of a given item.
            return
        end
        t[name] = (t[name] or 0) + count
    end
    -- Add to storage table of earned samples for future new players
    add_to_table(storage.forcedata[force.name]['earned_samples'])
    -- Add to existing players
    for _, player in pairs(force.players) do
        add_to_table(storage.playerdata[player.index]['pending_samples'])
        update_player(player.index)
    end
end

script.on_init(function()
    {% if not imported_blueprints %}set_permissions(){% endif %}
    storage.forcedata = {}
    storage.playerdata = {}
    storage.energy_link_bridges = {}
    -- Fire dummy events for all currently existing forces.
    local e = {}
    for name, _ in pairs(game.forces) do
        e.force = name
        on_force_created(e)
    end
    e.force = nil

    -- Fire dummy events for all currently existing players.
    for index, _ in pairs(game.players) do
        e.player_index = index
        on_player_created(e)
    end

    if remote.interfaces["silo_script"] then
        remote.call("silo_script", "set_no_victory", true)
    end
end)

-- hook into researches done
script.on_event(defines.events.on_research_finished, function(event)
    local technology = event.research
    if string.find(technology.force.name, "EE_TESTFORCE") == 1 then
        --Don't acknowledge AP research as an Editor Extensions test force
        --Also no need for free samples in the Editor extensions testing surfaces, as these testing surfaces
        --are worked on exclusively in editor mode.
        return
    end
    if technology.researched and string.find(technology.name, "ap%-") == 1 then
        -- check if it came from the server anyway, then we don't need to double send.
        dumpInfo(technology.force) --is sendable
    else
        if FREE_SAMPLES == 0 then
            return  -- Nothing else to do
        end
        if not technology.prototype.effects then
            return  -- No technology effects, so nothing to do.
        end
        for _, effect in pairs(technology.prototype.effects) do
            if effect.type == "unlock-recipe" then
                local recipe = prototypes.recipe[effect.recipe]
                for _, result in pairs(recipe.products) do
                    if result.type == "item" and result.amount then
                        local name = result.name
                        if FREE_SAMPLE_BLACKLIST[name] ~= 1 then
                            local count
                            if FREE_SAMPLES == 1 then
                                count = result.amount
                            else
                                count = get_any_stack_size(result.name)
                                if FREE_SAMPLES == 2 then
                                    count = math.ceil(count / 2)
                                end
                            end
                            add_samples(technology.force, name, count)
                        end
                    end
                end
            end
        end
    end
end)


function dumpInfo(force)
    log("Archipelago Bridge Data available for game tick ".. game.tick .. ".") -- notifies client
end


function chain_lookup(table, ...)
    for _, k in ipairs{...} do
        table = table[k]
        if not table then
            return nil
        end
    end
    return table
end

function kill_players(force)
    CURRENTLY_DEATH_LOCK = 1
    local current_character = nil
    for _, player in ipairs(force.players) do
        current_character = player.character
        if current_character ~= nil then
            current_character.die()
        end
    end
    CURRENTLY_DEATH_LOCK = 0
end

function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores)
    local prototype = prototypes.entity[name]
    local args = {  -- For can_place_entity and place_entity
        name = prototype.name,
        position = {x = x, y = y},
        force = force.name,
        build_check_type = defines.build_check_type.blueprint_ghost,
        forced = true
    }

    local box = prototype.selection_box
    local dims = {
        w = box.right_bottom.x - box.left_top.x,
        h = box.right_bottom.y - box.left_top.y
    }
    local entity_radius = math.ceil(math.max(dims.w, dims.h) / math.sqrt(2) / 2)
    local bounds = {
        xmin = math.ceil(x - radius - box.left_top.x),
        xmax = math.floor(x + radius - box.right_bottom.x),
        ymin = math.ceil(y - radius - box.left_top.y),
        ymax = math.floor(y + radius - box.right_bottom.y)
    }

    local new_entity = nil
    local attempts = 1000
    for i = 1,attempts do  -- Try multiple times
        -- Find a position
        if (randomize and i < attempts-3) or (not randomize and i ~= 1) then
            args.position.x = math.random(bounds.xmin, bounds.xmax)
            args.position.y = math.random(bounds.ymin, bounds.ymax)
        elseif randomize then
            args.position.x = x + (i + 3 - attempts) * dims.w
            args.position.y = y + (i + 3 - attempts) * dims.h
        end
        -- Generate required chunks
        local x1 = args.position.x + box.left_top.x
        local x2 = args.position.x + box.right_bottom.x
        local y1 = args.position.y + box.left_top.y
        local y2 = args.position.y + box.right_bottom.y
        if not surface.is_chunk_generated({x = x1, y = y1}) or
           not surface.is_chunk_generated({x = x2, y = y1}) or
           not surface.is_chunk_generated({x = x1, y = y2}) or
           not surface.is_chunk_generated({x = x2, y = y2}) then
            surface.request_to_generate_chunks(args.position, entity_radius)
            surface.force_generate_chunk_requests()
        end
        -- Try to place entity
        if surface.can_place_entity(args) then
            -- Can hypothetically place this entity here.  Destroy everything underneath it.
            local collision_area = {
                {
                    args.position.x + prototype.collision_box.left_top.x,
                    args.position.y + prototype.collision_box.left_top.y
                },
                {
                    args.position.x + prototype.collision_box.right_bottom.x,
                    args.position.y + prototype.collision_box.right_bottom.y
                }
            }
            local entities = surface.find_entities_filtered {
                area = collision_area,
                collision_mask = prototype.collision_mask.layers
            }
            local can_place = true
            for _, entity in pairs(entities) do
                if entity.force and entity.force.name ~= 'neutral' then
                    can_place = false
                    break
                end
            end
            local allow_placement_on_resources = not avoid_ores or i > attempts/2
            if can_place and not allow_placement_on_resources then
                local resources = surface.find_entities_filtered {
                    area = collision_area,
                    type = 'resource'
                }
                can_place = (next(resources) == nil)
            end
            if can_place then
                for _, entity in pairs(entities) do
                    entity.destroy({do_cliff_correction=true, raise_destroy=true})
                end
                args.build_check_type = defines.build_check_type.script
                args.create_build_effect_smoke = false
                if script.active_mods["quality"] then
                    args.quality = "{{ free_sample_quality_name }}"
                end
                new_entity = surface.create_entity(args)
                if new_entity then
                    new_entity.destructible = false
                    new_entity.minable = false
                    new_entity.rotatable = false
                    break
                end
            end
        end
    end
    if new_entity == nil then
        force.print("Failed to place " .. args.name .. " in " .. serpent.line({x = x, y = y, radius = radius}))
    end
end


script.on_event(defines.events.on_entity_died, function(event)
    if DEATH_LINK == 0 then
        return
    end
    if CURRENTLY_DEATH_LOCK == 1 then -- don't re-trigger on same event
        return
    end

    local force = event.entity.force
    storage.forcedata[force.name].death_link_tick = game.tick
    dumpInfo(force)
    kill_players(force)
end, {LuaEntityDiedEventFilter = {["filter"] = "name", ["name"] = "character"}})


-- add / commands
commands.add_command("ap-sync", "Used by the Archipelago client to get progress information", function(call)
    local force
    if call.player_index == nil then
        force = game.forces.player
    else
        force = game.players[call.player_index].force
    end
    local research_done = {}
    local forcedata = chain_lookup(storage, "forcedata", force.name)
    local data_collection = {
        ["research_done"] = research_done,
        ["victory"] = chain_lookup(forcedata, "victory"),
        ["death_link_tick"] = chain_lookup(forcedata, "death_link_tick"),
        ["death_link"] = DEATH_LINK,
        ["energy"] = chain_lookup(forcedata, "energy"),
        ["energy_bridges"] = chain_lookup(forcedata, "energy_bridges"),
        ["multiplayer"] = #game.players > 1,
    }

    for tech_name, tech in pairs(force.technologies) do
        if tech.researched and string.find(tech_name, "ap%-") == 1 then
            research_done[tech_name] = tech.researched
        end
    end
    rcon.print(helpers.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME, ["info"] = data_collection}))
end)

commands.add_command("ap-print", "Used by the Archipelago client to print messages", function (call)
    game.print(call.parameter)
end)

TRAP_TABLE = {
["Attack Trap"] = function ()
    game.surfaces["nauvis"].build_enemy_base(game.forces["player"].get_spawn_position(game.get_surface(1)), 25)
end,
["Evolution Trap"] = function ()
    local new_factor = game.forces["enemy"].get_evolution_factor("nauvis") +
        (TRAP_EVO_FACTOR * (1 - game.forces["enemy"].get_evolution_factor("nauvis")))
    game.forces["enemy"].set_evolution_factor(new_factor, "nauvis")
    game.print({"", "New evolution factor:", new_factor})
end,
["Teleport Trap"] = function()
    for _, player in ipairs(game.forces["player"].players) do
        if player.character then
            attempt_teleport_player(player, 1)
        end
    end
end,
["Grenade Trap"] = function ()
    fire_entity_at_players("grenade", 0.1)
end,
["Cluster Grenade Trap"] = function ()
    fire_entity_at_players("cluster-grenade", 0.1)
end,
["Artillery Trap"] = function ()
    fire_entity_at_players("artillery-projectile", 1)
end,
["Atomic Rocket Trap"] = function ()
    fire_entity_at_players("atomic-rocket", 0.1)
end,
["Atomic Cliff Remover Trap"] = function ()
    local cliffs = game.surfaces["nauvis"].find_entities_filtered{type = "cliff"}

    if #cliffs > 0 then
        fire_entity_at_entities("atomic-rocket", {cliffs[math.random(#cliffs)]}, 0.1)
    end
end,
["Inventory Spill Trap"] = function ()
    for _, player in ipairs(game.forces["player"].players) do
        spill_character_inventory(player.character)
    end
end,
}

commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)
    if storage.index_sync == nil then
        storage.index_sync = {}
    end
    local tech
    local force = game.forces["player"]
    if call.parameter == nil then
        game.print("ap-get-technology is only to be used by the Archipelago Factorio Client")
        return
    end
    chunks = split(call.parameter, "\t")
    local item_name = chunks[1]
    local index = chunks[2]
    local source = chunks[3] or "Archipelago"
    if index == nil then
        game.print("ap-get-technology is only to be used by the Archipelago Factorio Client")
        return
    elseif index == -1 then -- for coop sync and restoring from an older savegame
        tech = force.technologies[item_name]
        if tech.researched ~= true then
            game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
            game.play_sound({path="utility/research_completed"})
            tech.researched = true
        end
        return
    elseif progressive_technologies[item_name] ~= nil then
        if storage.index_sync[index] ~= item_name then -- not yet received prog item
            storage.index_sync[index] = item_name
            local tech_stack = progressive_technologies[item_name]
            for _, item_name in ipairs(tech_stack) do
                tech = force.technologies[item_name]
                if tech.researched ~= true then
                    game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
                    game.play_sound({path="utility/research_completed"})
                    tech.researched = true
                    return
                end
            end
        end
    elseif force.technologies[item_name] ~= nil then
        tech = force.technologies[item_name]
        if tech ~= nil then
            storage.index_sync[index] = tech
            if tech.researched ~= true then
                game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
                game.play_sound({path="utility/research_completed"})
                tech.researched = true
            end
        end
    elseif TRAP_TABLE[item_name] ~= nil then
        if storage.index_sync[index] ~= item_name then -- not yet received trap
            storage.index_sync[index] = item_name
            game.print({"", "Received ", item_name, " from ", source})
            TRAP_TABLE[item_name]()
        end
    else
        game.print("Unknown Item " .. item_name)
    end
end)


commands.add_command("ap-rcon-info", "Used by the Archipelago client to get information", function(call)
    rcon.print(helpers.table_to_json({
        ["slot_name"] = SLOT_NAME,
        ["seed_name"] = SEED_NAME,
        ["death_link"] = DEATH_LINK,
        ["energy_link"] = ENERGY_INCREMENT
    }))
end)


{% if allow_cheats -%}
commands.add_command("ap-spawn-silo", "Attempts to spawn a silo and cargo landing pad around 0,0", function(call)
    spawn_entity(game.player.surface, game.player.force, "rocket-silo", 0, 0, 80, true, true)
end)
{% endif -%}


commands.add_command("ap-deathlink", "Kill all players", function(call)
    local force = game.forces["player"]
    local source = call.parameter or "Archipelago"
    kill_players(force)
    game.print("Death was granted by " .. source)
end)

commands.add_command("ap-energylink", "Used by the Archipelago client to manage Energy Link", function(call)
    local change = tonumber(call.parameter or "0")
    local force = "player"
    storage.forcedata[force].energy = storage.forcedata[force].energy + change
end)

commands.add_command("energy-link", "Print the status of the Archipelago energy link.", function(call)
    log("Player command energy-link") -- notifies client
end)

commands.add_command("toggle-ap-send-filter", "Toggle filtering of item sends that get displayed in-game to only those that involve you.", function(call)
    log("Player command toggle-ap-send-filter") -- notifies client
end)

commands.add_command("toggle-ap-chat", "Toggle sending of chat messages from players on the Factorio server to Archipelago.", function(call)
    log("Player command toggle-ap-chat") -- notifies client
end)

-- data
progressive_technologies = {{ dict_to_lua(progressive_technology_table) }}
