map_gen/shared/redmew_surface.lua
--[[
    Creates a custom surface for all redmew maps so that we can ignore all user input at time of world creation.

    Allows map makers to define the map gen settings, map settings, and difficulty settings in as much or as little details as they want.
    The aim is to make this a very easy process for map makers, while eliminating the need for some of the existing builder functions.
    For example by preventing ores from spawning we no longer need to manually scan for and remove ores.

    When you create a new map you're given many options. These options break into 3 categories which are not made explicitly
    clear in the game itself:
    map_gen_settings, map_settings, and difficulty_settings

    map_gen_settings: Only affect a given surface. These settings determine everything that surface is made of:
    ores, tiles, entities, boundaries, etc. It also contains a less obvious setting: peaceful_mode.

    map_settings: Are kind of a misnomer since they apply to the game at large. Contain settings for pollution, enemy_evolution, enemy_expansion,
    unit_group, steering, path_finder, and something called max_failed_behavior_count (shrug)
    lastly, difficulty_settings

    difficulty_settings: contains only recipe_difficulty, technology_difficulty (not used in vanilla), and technology_price_multiplier

    In the 16.51 version of factorio's Map Generator page difficulty_settings make up the "Recipes/Technology" section of the
    "Advanced settings" tab. map_settings make up the rest of that tab.
    map_gen_settings are detemined by everything in the remaining 3 tabs (Basic settings, Resource settings, Terrain settings)

    Unless fed arguments via the public functions, this module will simply clone nauvis, respecting all user settings.
    To pass settings to redmew_surface, each of the above-mentioned 3 settings components has a public function.
    set_map_gen_settings, set_map_settings, and set_difficulty_settings
    The functions all take a list of tables which contain settings. The tables then overwrite any existing user settings.
    Therefore, for any settings not explicitly set the user's settings persist.

    Tables of settings can be constructed manually or can be taken from the resource files by the same names (resources/map_gen_settings, etc.)

    Example: to select a 4x tech cost you would call:
    RS.set_difficulty_settings({difficulty_settings_presets.tech_x4})

    It should be noted that tables earlier in the list will be overwritten by tables later in the list.
    So in the following example the resulting tech cost would be 4, not 3.

    RS.set_difficulty_settings({difficulty_settings_presets.tech_x3, difficulty_settings_presets.tech_x4})

    To create a map with no ores, no enemies, no pollution, no enemy evolution, 3x tech costs, and sand set to high we would use the following:

    -- We require redmew_surface to access the public functions and assign the table Public to the RS variable to access them easily.
    local RS = require 'map_gen.shared.redmew_surface'
    -- We require the resources tables so that we don't have to write settings components by hand.
    local MGSP = require 'resources.map_gen_settings' -- map gen settings presets
    local DSP = require 'resources.difficulty_settings' -- difficulty settings presets
    local MSP = require 'resources.map_settings' -- map settings presets

    -- We create a custom table for the niche settings of wanting more sand
    local extra_sand = {
        autoplace_controls = {
            sand = {frequency = 'high', size = 'high'}
        }
    }

    RS.set_map_gen_settings({MGSP.enemy_none, MGSP.ore_none, MGSP.oil_none, extra_sand})
    RS.set_difficulty_settings({DSP.tech_x3})
    RS.set_map_settings({MSP.enemy_evolution_off, MSP.pollution_off})
]]
-- Dependencies
require 'util'
local Event = require 'utils.event'
local Global = require 'utils.global'
local config = storage.config.redmew_surface

-- Localized functions
local insert = table.insert
local merge = util.merge
local format = string.format

-- Constants
local set_warn_message = 'set_%s has already been called. Calling this twice can lead to unexpected settings overwrites.'
local vanilla_surface_name = 'nauvis'
local redmew_surface_name = 'redmew'

-- Local vars
local set_difficulty_settings_called
local set_map_gen_settings_called
local set_map_settings_called
local data = {
    ['map_gen_settings_components'] = {},
    ['map_settings_components'] = {},
    ['difficulty_settings_components'] = {}
}
local Public = {}

-- Global tokens
-- The nil definitions are to document what data might exist.
local global_data = {
    surface = nil,
    first_player_position_check_override = nil,
    spawn_position = nil,
    island_tile = nil
}

Global.register(
    global_data,
    function(tbl)
        global_data = tbl
    end
)

-- Local functions

--- Add the tables inside components into the given data_table
local function combine_settings(components, data_table)
    for _, v in pairs(components) do
        insert(data_table, v)
    end
end

--- Sets up the difficulty settings
local function set_difficulty_settings()
    local combined_difficulty_settings = merge(data.difficulty_settings_components)
    for k, v in pairs(combined_difficulty_settings) do
        game.difficulty_settings[k] = v
    end
end

--- Sets up the map settings
local function set_map_settings()
    local combined_map_settings = merge(data.map_settings_components)

    -- Iterating through individual tables because game.map_settings is read-only
    if combined_map_settings.pollution then
        for k, v in pairs(combined_map_settings.pollution) do
            game.map_settings.pollution[k] = v
        end
    end
    if combined_map_settings.enemy_evolution then
        for k, v in pairs(combined_map_settings.enemy_evolution) do
            game.map_settings.enemy_evolution[k] = v
        end
    end
    if combined_map_settings.enemy_expansion then
        for k, v in pairs(combined_map_settings.enemy_expansion) do
            game.map_settings.enemy_expansion[k] = v
        end
    end
    if combined_map_settings.unit_group then
        for k, v in pairs(combined_map_settings.unit_group) do
            game.map_settings.unit_group[k] = v
        end
    end
    if combined_map_settings.steering then
        if combined_map_settings.steering.default then
            for k, v in pairs(combined_map_settings.steering.default) do
                game.map_settings.steering.default[k] = v
            end
        end
        if combined_map_settings.steering.moving then
            for k, v in pairs(combined_map_settings.steering.moving) do
                game.map_settings.steering.moving[k] = v
            end
        end
    end
    if combined_map_settings.path_finder then
        for k, v in pairs(combined_map_settings.path_finder) do
            game.map_settings.path_finder[k] = v
        end
    end
    if combined_map_settings.max_failed_behavior_count then
        game.map_settings.max_failed_behavior_count = combined_map_settings.max_failed_behavior_count
    end
end

local function create_surface_with_settings()
    -- Combine default map gen settings with user components
    local combined_map_gen = { game.surfaces.nauvis.map_gen_settings }
    local nauvis_as = combined_map_gen[1].autoplace_settings

    for _, v in pairs(data.map_gen_settings_components) do
        table.insert(combined_map_gen, v)
        -- Check for autoplace setting blacklisting
        local as = v.autoplace_settings
        if nauvis_as and as then
            for _, name in pairs({ 'entity', 'tile', 'decorative' }) do
                if nauvis_as[name] and as[name] and as[name].treat_missing_as_default == false then
                    nauvis_as[name].settings = {}
                end
            end
        end
    end

    local surface
    if config.use_default then
        surface = game.surfaces[vanilla_surface_name]
        surface.map_gen_settings = merge(combined_map_gen)
    else
        surface = game.create_surface(redmew_surface_name, merge(combined_map_gen))
    end
    return surface
end

local function get_or_create_surface()
    if config.map_gen_settings then
        return create_surface_with_settings()
    else
        if config.use_default then
            return game.surfaces[vanilla_surface_name]
        else
            return game.create_surface(redmew_surface_name)
        end
    end
end

local function initialize_surface()
    local surface = global_data.surface
    if not surface then
        return
    end

    if config.use_default then
        surface.clear(true)
    else
        -- Hide the vanilla surface to all forces
        for _, force in pairs(game.forces) do
            force.set_surface_hidden(vanilla_surface_name, true)
        end
    end

    surface.request_to_generate_chunks({0, 0}, 4)
    surface.force_generate_chunk_requests()

    local spawn_position = global_data.spawn_position
    if spawn_position then
        game.forces.player.set_spawn_position(spawn_position, surface)
    end
end

--- Creates a new surface with the settings provided by the map file and the player.
local function create_scenario_surface()
    if not config.enabled then
        -- Still need to set the surface for Public.get_surface() to work
        global_data.surface = game.surfaces[vanilla_surface_name]
        return
    end

    global_data.surface = get_or_create_surface()

    if config.difficulty then
        set_difficulty_settings()
    end
    if config.map_settings then
        set_map_settings()
    end

    initialize_surface()
end

--- Teleport the player to the redmew surface and if there is no suitable location, create an island
local function player_created(event)
    local player = game.get_player(event.player_index)
    local surface = global_data.surface

    local spawn_position = global_data.spawn_position or {x = 0, y = 0}
    local pos = surface.find_non_colliding_position('character', spawn_position, 50, 1)

    if pos and not global_data.first_player_position_check_override then -- we tp to that pos
        player.teleport(pos, surface)
    else
        -- if there's no position available within range or we override the position check:
        -- create an island and place the player at spawn_position
        local island_tile = global_data.island_tile or 'lab-white'
        local tile_table = {}
        for x = -1, 1 do
            for y = -1, 1 do
                insert(tile_table, {name = island_tile, position = {spawn_position.x - x, spawn_position.y - y}})
            end
        end
        surface.set_tiles(tile_table)

        player.teleport(spawn_position, surface)
        global_data.first_player_position_check_override = nil
    end
end

-- Public functions

--- Sets components to the difficulty_settings_components table
-- It is an error to call this twice as later calls will overwrite earlier ones if values overlap.
-- @param components <table> list of difficulty settings components (usually from resources.difficulty_settings)
function Public.set_difficulty_settings(components)
    if set_difficulty_settings_called then
        log(format(set_warn_message, 'difficulty_settings'))
    end
    combine_settings(components, data.difficulty_settings_components)
    set_difficulty_settings_called = true
end

--- Adds components to the map_gen_settings_components table
-- It is an error to call this twice as later calls will overwrite earlier ones if values overlap.
-- @param components <table> list of map gen components (usually from resources.map_gen_settings)
function Public.set_map_gen_settings(components)
    if set_map_gen_settings_called then
        log(format(set_warn_message, 'map_gen_settings'))
    end
    combine_settings(components, data.map_gen_settings_components)
    set_map_gen_settings_called = true
end

--- Adds components to the map_settings_components table
-- It is an error to call this twice as later calls will overwrite earlier ones if values overlap.
-- @param components <table> list of map setting components (usually from resources.map_settings)
function Public.set_map_settings(components)
    if set_map_settings_called then
        log(format(set_warn_message, 'map_settings'))
    end
    combine_settings(components, data.map_settings_components)
    set_map_settings_called = true
end

--- Returns the LuaSurface that the map is created on.
-- Not safe to call outside of events.
function Public.get_surface()
    return global_data.surface
end

--- Returns the string name of the surface that the map is created on.
-- This can safely be called at any time.
function Public.get_surface_name()
    if config.enabled and not config.use_default then
        return redmew_surface_name
    else
        return vanilla_surface_name
    end
end

--- Allows maps to skip the collision check for the first player being teleported.
-- This is useful when a collision check at the spawn point is either invalid or puts the
-- player in a position that will get them killed by map generation (ex. diggy, tetris)
function Public.set_first_player_position_check_override(bool)
    global_data.first_player_position_check_override = bool
end

--- Allows maps to set a custom spawn position
-- @param position <table> with x and y keys ex.{x = 5.0, y = 5.0}
function Public.set_spawn_position(position)
    global_data.spawn_position = position
end

--- Allows maps to set the tile used for spawn islands
-- @param tile_name <string> name of the tile to create the island out of
function Public.set_spawn_island_tile(tile_name)
    global_data.island_tile = tile_name
end

Event.on_init(create_scenario_surface)

if config.enabled then
    Event.add(defines.events.on_player_created, player_created)
end

return Public
