1
0
mirror of https://github.com/Oarcinae/FactorioScenarioMultiplayerSpawn.git synced 2024-12-12 10:13:58 +02:00
FactorioScenarioMultiplayer.../lib/separate_spawns.lua

1844 lines
76 KiB
Lua

-- Code that handles everything regarding giving each player a separate spawn
local util = require("util")
local crash_site = require("crash-site")
require("lib/spawn_area_generation")
--[[
___ _ _ ___ _____
|_ _|| \| ||_ _||_ _|
| | | .` | | | | |
|___||_|\_||___| |_|
--]]
-- Hardcoded force names for special cases.
ABANDONED_FORCE_NAME = "_ABANDONED_"
-- DESTROYED_FORCE_NAME = "_DESTROYED_"
---Initializes the globals used to track the special spawn and player status information.
---@return nil
function InitSpawnGlobalsAndForces()
-- Contains a table of entries for each surface. This tracks which surfaces allow spawning?
--[[@type table<string, OarcSurfaceSpawnSetting>]]
storage.oarc_surfaces = {}
for _, surface in pairs(game.surfaces) do
SeparateSpawnsInitSurface(surface.name)
end
SeparateSpawnsInitPlanets()
-- This contains each player's respawn point. Literally where they will respawn on death
-- There is a way in game to change this under one of the little menu features I added. This allows players to
-- change their respawn point to something other than their home base.
-- TODO: Space Age will potentially affect this, as I may need to allow for multiple respawn points on different surfaces.
--[[@type OarcPlayerRespawnsTable]]
storage.player_respawns = {}
-- This is the most important table. It is a list of all the unique spawn points.
-- This is what chunk generation checks against, and is used for shared spawn tracking, and more.
---@type OarcUniqueSpawnsTable
storage.unique_spawns = {}
-- Each player has an option to change their respawn which has a cooldown when used.
-- Other similar abilities/functions that require cooldowns could be added here.
--[[@type OarcPlayerCooldownsTable]]
storage.player_cooldowns = {}
-- Players who have made a spawn choice get put into this list while waiting.
-- An on_tick event checks when it expires and then places down the base resources, and teleports the player.
-- Go look at DelayedSpawnOnTick() for more info.
--[[@type OarcDelayedSpawnsTable]]
storage.delayed_spawns = {}
-- This stores the spawn choices that a player makes from the GUI interactions.
-- Intended to be re-used for secondary spawns! (TODO SPACE AGE)
--[[@type OarcSpawnChoicesTable]]
storage.spawn_choices = {}
-- Buddy info: The only real use is to check if one of a buddy pair is online to see if we should allow enemy
-- attacks on the base. <br>
-- storage.buddy_pairs[player.name] = requesterName <br>
-- storage.buddy_pairs[requesterName] = player.name <br>
--[[@type table<string, string>]]
storage.buddy_pairs = {}
--- Table contains all the renders that need to be faded out over time in the on_tick event. They are removed when they expire.
--[[@type table<integer>]]
storage.oarc_renders_fadeout = {}
-- This is a queue of players that need to be teleported to their spawn point.
--[[@type table<string, OarcNilCharacterTeleportQueueEntry>]]
storage.nil_character_teleport_queue = {}
-- Special forces for when players with their own force want a reset.
game.create_force(ABANDONED_FORCE_NAME)
-- game.create_force(DESTROYED_FORCE_NAME)
-- Name a new force to be the default force.
-- This is what any new player is assigned to when they join, even before they spawn.
local main_force = CreatePlayerForce(storage.ocfg.gameplay.main_force_name)
main_force.set_spawn_position({ x = 0, y = 0 }, storage.ocfg.gameplay.default_surface)
CreateHoldingPenPermissionsGroup()
end
function CreateHoldingPenPermissionsGroup()
-- Create a permission group for the holding pen players.
if (game.permissions.get_group("holding_pen") == nil) then
game.permissions.create_group("holding_pen")
end
local holding_pen_group = game.permissions.get_group("holding_pen")
-- Disable all permissions for the holding pen group.
for _,action in pairs(defines.input_action) do
holding_pen_group.set_allows_action(action, false)
end
-- Just allow the ones we want:
holding_pen_group.set_allows_action(defines.input_action.gui_checked_state_changed, true)
holding_pen_group.set_allows_action(defines.input_action.gui_click, true)
holding_pen_group.set_allows_action(defines.input_action.gui_confirmed, true)
holding_pen_group.set_allows_action(defines.input_action.gui_elem_changed, true)
holding_pen_group.set_allows_action(defines.input_action.gui_hover, true)
holding_pen_group.set_allows_action(defines.input_action.gui_leave, true)
holding_pen_group.set_allows_action(defines.input_action.gui_location_changed, true)
holding_pen_group.set_allows_action(defines.input_action.gui_selected_tab_changed, true)
holding_pen_group.set_allows_action(defines.input_action.gui_selection_state_changed, true)
holding_pen_group.set_allows_action(defines.input_action.gui_switch_state_changed, true)
holding_pen_group.set_allows_action(defines.input_action.gui_text_changed, true)
holding_pen_group.set_allows_action(defines.input_action.gui_value_changed, true)
holding_pen_group.set_allows_action(defines.input_action.start_walking, true)
holding_pen_group.set_allows_action(defines.input_action.write_to_console, true)
end
---Detects when new surfaces are created and inits them. Does not trigger during on_init?
---@param event EventData.on_surface_created
---@return nil
function SeparateSpawnsSurfaceCreated(event)
local surface_name = game.surfaces[event.surface_index].name
log("SeparateSpawnsSurfaceCreated - " .. surface_name)
SeparateSpawnsInitSurface(surface_name)
end
---Init globals for a new surface and set the default allow spawn value based on settings.
---@param surface_name string
---@return nil
function SeparateSpawnsInitSurface(surface_name)
-- Shouldn't happen because surface created isn't triggered during on_init.
if (storage.oarc_surfaces == nil) then
error("storage.oarc_surfaces not initialized yet?! " .. surface_name)
end
if IsSurfaceBlacklisted(surface_name) then return end
if storage.oarc_surfaces[surface_name] == nil then
-- Default surface is set to primary only, all others are secondary only if
-- default_enable_secondary_spawns_on_other_surfaces is set to true.
if (surface_name == storage.ocfg.gameplay.default_surface) then
storage.oarc_surfaces[surface_name] = {
primary = true,
secondary = false
}
else
storage.oarc_surfaces[surface_name] = {
primary = false,
secondary = storage.ocfg.gameplay.default_enable_secondary_spawns_on_other_surfaces
}
end
end
-- Make sure it has a surface configuration entry!
if (storage.ocfg.surfaces_config[surface_name] == nil) then
log("Surface does NOT have a config entry, defaulting to nauvis entry for new surface: " .. surface_name)
storage.ocfg.surfaces_config[surface_name] = table.deepcopy(storage.ocfg.surfaces_config["nauvis"])
end
end
---Init surface globals using game.planets
---@return nil
function SeparateSpawnsInitPlanets()
for _, planet in pairs(game.planets) do
SeparateSpawnsInitSurface(planet.name)
end
end
---Detects when surfaces are deleted and removes them from the list of surfaces that allow spawns.
---@param event EventData.on_pre_surface_deleted
---@return nil
function SeparateSpawnsSurfaceDeleted(event)
local surface_name = game.surfaces[event.surface_index].name
-- Remove the surface from the list of surfaces that allow spawns
if storage.oarc_surfaces[surface_name] ~= nil then
log("WARNING!! - Surface deleted event not validated/implemented yet! " .. surface_name)
storage.oarc_surfaces[surface_name] = nil
-- TODO: Validate if we need to do other cleanup too, like unique spawns, etc. I can't
-- think of a reason why we would need to do that yet.
end
end
--[[
___ _ _ __ __ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
| _ \| | /_\\ \ / /| __|| _ \ / __|| _ \| __|/ __||_ _|| __||_ _|/ __|
| _/| |__ / _ \\ V / | _| | / \__ \| _/| _|| (__ | | | _| | || (__
|_| |____|/_/ \_\|_| |___||_|_\ |___/|_| |___|\___||___||_| |___|\___|
--]]
-- When a player is newly created or just reset, present the spawn options to them.
-- If new player, assign them to the main force so they can communicate with the team without shouting (/s).
---@param player_index integer|string
---@return nil
function SeparateSpawnsInitPlayer(player_index)
local player = game.players[player_index]
SafeTeleport(player, game.surfaces[HOLDING_PEN_SURFACE_NAME], { x = 0, y = 0 })
player.force = storage.ocfg.gameplay.main_force_name -- Put them on the main force.
if (not player.admin) then
player.permission_group = game.permissions.get_group("holding_pen")
end
if (storage.player_respawns[player.name] == nil) then
storage.player_respawns[player.name] = {}
end
if (storage.player_cooldowns[player.name] == nil) then
storage.player_cooldowns[player.name] = { setRespawn = game.tick }
end
-- Reset GUI and show the spawn options.
DisplayWelcomeTextGui(player)
InitOarcGuiTabs(player)
HideOarcGui(player)
SetOarcGuiTabEnabled(player, OARC_SPAWN_CTRL_TAB_NAME, false) -- Make sure spawn control tab is disabled
SwitchOarcGuiTab(player, OARC_SERVER_INFO_TAB_NAME)
end
-- Check if the player has a different spawn point than the default one
-- Make sure to give the default starting items
---@param event EventData.on_player_respawned
---@return nil
function SeparateSpawnsPlayerRespawned(event)
local player = game.players[event.player_index]
local surface_name = player.character.surface.name
-- It's possible if player is dead, and then resets, we don't want to do anything else.
if (surface_name == HOLDING_PEN_SURFACE_NAME) then return end
-- If the mod isn't active on this surface, then ignore it.
local surface_config = storage.oarc_surfaces[surface_name]
if (surface_config == nil) or
(not surface_config.primary and not surface_config.secondary) then
return
end
TeleportPlayerToRespawnPoint(surface_name, player, false)
GivePlayerRespawnItems(player)
end
---If the player leaves early, remove their base.
---@param event EventData.on_player_left_game
---@return nil
function SeparateSpawnsPlayerLeft(event)
local player = game.players[event.player_index]
-- If players leave early, say goodbye.
if (player and (player.online_time < (storage.ocfg.gameplay.minimum_online_time * TICKS_PER_MINUTE))) then
SendBroadcastMsg({ "oarc-player-left-early", player.name, storage.ocfg.gameplay.minimum_online_time })
RemoveOrResetPlayer(player, true)
end
end
---If the player moves surfaces, check if we need to present them with new a new spawn.
---@param player LuaPlayer
---@param previous_surface_name string?
---@param new_surface_name string
---@return nil
function SeparateSpawnsPlayerChangedSurface(player, previous_surface_name, new_surface_name)
if (previous_surface_name == nil) then return end
log("SeparateSpawnsPlayerChangedSurface from " .. previous_surface_name .. " to " .. new_surface_name)
-- TODO make sure this isn't too spammy?
local surface = game.surfaces[new_surface_name]
local platform = surface.platform
if (platform ~= nil) then
SendBroadcastMsg({ "oarc-player-on-platform", player.name, surface.platform.index }, { color = player.color})
else
if (game.planets[new_surface_name] ~= nil) then
SendBroadcastMsg({ "oarc-player-changed-surface", player.name, "[planet=" .. new_surface_name .. "]" }, { color = player.color, sound_path = "utility/new_objective"})
else
SendBroadcastMsg({ "oarc-player-changed-surface", player.name, surface.localised_name or { "space-location-name." .. surface.name } or surface.name}, { color = player.color})
end
end
-- Check if player has been init'd yet. If not, then ignore it.
if (storage.player_respawns[player.name] == nil) then return end
-- If the mod isn't active on this surface, then ignore it.
local surface_config = storage.oarc_surfaces[new_surface_name]
if (surface_config == nil) or
(not surface_config.primary and not surface_config.secondary) then
return
end
-- If previous surface was a platform
-- local arriving_from_space = StringStartsWith(previous_surface_name, "platform-")
-- If we are NOT arriving from space, then ignore the rest of this??
-- if (not arriving_from_space) then return end
-- Check if there is already a landing pad for their force on the surface
local landing_pad = surface.find_entities_filtered{name = "cargo-landing-pad", force = player.force, limit = 1}
if (#landing_pad > 0) then
-- If there's a landing pad for the force, I don't think we should interfere with the native behavior until
-- there is an API that lets us do it properly. Otherwise I'm having to track landing pads per spawn...
-- TODO: Implement this properly.
if (player.force.name ~= storage.ocfg.gameplay.main_force_name) then
log("Player has a landing pad on this surface, let them land there?")
else
log("WARNING - I haven't fully implemented this yet! Player has a landing pad on this surface but is on the main force so it might not be at their own spawn?!")
end
return
end
local player_spawn = FindPlayerSpawnOnSurface(player.name, new_surface_name) -- Either they are host or joiner.
-- If there IS a spawn for them on their new surface
if (player_spawn ~= nil) then
-- Then just send them to their respawn point which they should have.
if (player_spawn.generated) then
TeleportPlayerToRespawnPoint(new_surface_name, player, false)
return
-- Unless they have to wait for it to be generated.
else
local delayed_spawn = FindDelayedSpawn(player.name, new_surface_name)
if (delayed_spawn == nil) then
error("FindPlayerSpawnOnSurface is ungenerated but FindDelayedSpawn returned nil? " .. player.name)
end
QueuePlayerForSpawn(player.name, delayed_spawn)
return
end
end
-- Check if secondary spawns are disabled
if (not storage.oarc_surfaces[new_surface_name].secondary) then
return
end
-- If there is no spawn for them on their new surface, generate one based on previous choices.
log("WARNING - SECONDARY SPAWNS ARE STILL EXPERIMENTAL!!")
SecondarySpawn(player, new_surface_name)
end
---Updates the player's surface and raises an event if it changes.
---@param player LuaPlayer
---@param new_surface_name string
---@return nil
function SeparateSpawnsUpdatePlayerSurface(player, new_surface_name)
if (storage.player_surfaces == nil) then
storage.player_surfaces = {}
end
local previous_surface_name = storage.player_surfaces[player.name]
if (previous_surface_name ~= new_surface_name) then
storage.player_surfaces[player.name] = new_surface_name
-- Raise event if previous surface isn't nil (avoids first spawn event)
if (previous_surface_name ~= nil) then
script.raise_event("oarc-mod-character-surface-changed", {
player_index=player.index,
old_surface_name=previous_surface_name,
new_surface_name=new_surface_name
})
end
end
end
--[[
___ ___ _ __ __ _ _ ___ ___ _____ _ _ ___
/ __|| _ \ /_\\ \ / /| \| | / __|| __||_ _|| | | || _ \
\__ \| _// _ \\ \/\/ / | .` | \__ \| _| | | | |_| || _/
|___/|_| /_/ \_\\_/\_/ |_|\_| |___/|___| |_| \___/ |_|
--]]
---Generate liquid strip
---@param delayed_spawn OarcDelayedSpawn
---@param spawn_config OarcConfigSpawn
---@param surface LuaSurface
---@return nil
function GenerateStartingLiquidStrip(delayed_spawn, spawn_config, surface)
local water_data = spawn_config.water
-- Reference position is the top of the spawn area.
local reference_pos = {
x = delayed_spawn.position.x,
y = delayed_spawn.position.y - (storage.ocfg.spawn_general.spawn_radius_tiles * spawn_config.radius_modifier)
}
CreateTileStrip(surface,
{ x = reference_pos.x + water_data.x_offset, y = reference_pos.y + water_data.y_offset },
water_data.length,
spawn_config.liquid_tile)
CreateTileStrip(surface,
{ x = reference_pos.x + water_data.x_offset, y = reference_pos.y + water_data.y_offset + 1 },
water_data.length,
spawn_config.liquid_tile)
end
-- Generate the basic starter resource around a given location.
---@param surface LuaSurface
---@param position TilePosition --The center of the spawn area
---@return nil
function GenerateStartingResources(surface, position)
--TODO: This should come from map gen settings instead:
local size_mod = storage.ocfg.resource_placement.size_multiplier
local amount_mod = storage.ocfg.resource_placement.amount_multiplier
local spawn_general = storage.ocfg.spawn_general
-- Generate all resource tile patches
-- Generate resources in random order around the spawn point.
if storage.ocfg.resource_placement.enabled then
if (spawn_general.shape == SPAWN_SHAPE_CHOICE_CIRCLE) or (spawn_general.shape == SPAWN_SHAPE_CHOICE_OCTAGON) then
PlaceResourcesInSemiCircle(surface, position, size_mod, amount_mod)
elseif (spawn_general.shape == SPAWN_SHAPE_CHOICE_SQUARE) then
PlaceResourcesInSquare(surface, position, size_mod, amount_mod)
end
-- Generate resources using specified offsets if auto placement is disabled.
else
for r_name, r_data in pairs(storage.ocfg.surfaces_config[surface.name].spawn_config.solid_resources --[[@as table<string, OarcConfigSolidResource>]]) do
local pos = { x = position.x + r_data.x_offset, y = position.y + r_data.y_offset }
GenerateResourcePatch(surface, r_name, r_data.size * size_mod, pos, r_data.amount * amount_mod)
end
end
local spawn_config = storage.ocfg.surfaces_config[surface.name].spawn_config
local radius = spawn_general.spawn_radius_tiles * spawn_config.radius_modifier
-- Generate special fluid resource patches (oil)
-- Autoplace using spacing and vertical offset.
-- Reference position is the bottom of the spawn area.
if storage.ocfg.resource_placement.enabled then
local y_offset = storage.ocfg.resource_placement.distance_to_edge
local fluid_ref_pos = { x = position.x, y = position.y + radius - y_offset }
local fluid_resources = storage.ocfg.surfaces_config[surface.name].spawn_config.fluid_resources
if fluid_resources ~= nil then
for r_name, r_data in pairs(fluid_resources) do
local spacing = r_data.spacing
local oil_patch_x = fluid_ref_pos.x - (((r_data.num_patches-1) * spacing) / 2)
local oil_patch_y = fluid_ref_pos.y
for i = 1, r_data.num_patches do
surface.create_entity({
name = r_name,
amount = r_data.amount,
position = { oil_patch_x, oil_patch_y }
})
oil_patch_x = oil_patch_x + spacing
end
fluid_ref_pos.y = fluid_ref_pos.y - spacing
end
end
-- This places using specified offsets if auto placement is disabled.
else
local fluid_ref_pos = { x = position.x, y = position.y + radius }
local fluid_resources = storage.ocfg.surfaces_config[surface.name].spawn_config.fluid_resources
if fluid_resources ~= nil then
for r_name, r_data in pairs(fluid_resources) do
local oil_patch_x = fluid_ref_pos.x + r_data.x_offset_start
local oil_patch_y = fluid_ref_pos.y + r_data.y_offset_start
for i = 1, r_data.num_patches do
surface.create_entity({
name = r_name,
amount = r_data.amount,
position = { oil_patch_x, oil_patch_y }
})
oil_patch_x = oil_patch_x + r_data.x_offset_next
oil_patch_y = oil_patch_y + r_data.y_offset_next
end
end
end
end
end
---Places starting resource deposits in a semi-circle around the spawn point.
---@param surface LuaSurface
---@param position TilePosition --The center of the spawn area
---@param size_mod number
---@param amount_mod number
---@return nil
function PlaceResourcesInSemiCircle(surface, position, size_mod, amount_mod)
-- Create list of resource tiles
---@type table<string>
local r_list = {}
local solid_resources = storage.ocfg.surfaces_config[surface.name].spawn_config.solid_resources
if solid_resources ~= nil then
for r_name, _ in pairs(solid_resources) do
if (r_name ~= "") then
table.insert(r_list, r_name)
end
end
end
-- Gleba style resources like plants
local gleba_resources = storage.ocfg.surfaces_config[surface.name].spawn_config.gleba_resources
if gleba_resources ~= nil then
for g_name, _ in pairs(gleba_resources) do
if (g_name ~= "") then
table.insert(r_list, g_name)
end
end
end
---@type table<string>
local shuffled_list = FYShuffle(r_list)
local num_resources = table_size(shuffled_list)
-- This places resources in a semi-circle
local surface_config = storage.ocfg.surfaces_config[surface.name]
local angle_offset_radians = math.rad(storage.ocfg.resource_placement.angle_offset)
local angle_final_radians = math.rad(storage.ocfg.resource_placement.angle_final)
local radius = storage.ocfg.spawn_general.spawn_radius_tiles * surface_config.spawn_config.radius_modifier - storage.ocfg.resource_placement.distance_to_edge
-- Special case for only one resource, place it in the middle of the semi-circle.
if (num_resources == 1) then
local r_name = shuffled_list[1]
local angle = ((angle_final_radians - angle_offset_radians) / 2) + angle_offset_radians;
local tx = (radius * math.cos(angle)) + position.x
local ty = (radius * math.sin(angle)) + position.y
local pos = { x = math.floor(tx), y = math.floor(ty) }
local resourceConfig = surface_config.spawn_config.solid_resources[r_name]
if (resourceConfig ~= nil) then
GenerateResourcePatch(surface, r_name, resourceConfig.size * size_mod, pos, resourceConfig.amount * amount_mod)
else
local gResourceConfig = surface_config.spawn_config.gleba_resources[r_name]
GenerateGlebaStyleResourcePatch(surface, gResourceConfig, gResourceConfig.size * size_mod, pos)
end
else
local theta = ((angle_final_radians - angle_offset_radians) / (num_resources-1));
local count = 0
for _, r_name in pairs(shuffled_list) do
local angle = (theta * count) + angle_offset_radians;
local tx = (radius * math.cos(angle)) + position.x
local ty = (radius * math.sin(angle)) + position.y
local pos = { x = math.floor(tx), y = math.floor(ty) }
local resourceConfig = surface_config.spawn_config.solid_resources[r_name]
if (resourceConfig ~= nil) then
GenerateResourcePatch(surface, r_name, resourceConfig.size * size_mod, pos, resourceConfig.amount * amount_mod)
else
local gResourceConfig = surface_config.spawn_config.gleba_resources[r_name]
GenerateGlebaStyleResourcePatch(surface, gResourceConfig, gResourceConfig.size * size_mod, pos)
end
count = count + 1
end
end
end
---Places starting resource deposits in a line starting at the top left of the spawn point.
---@param surface LuaSurface
---@param position TilePosition --The center of the spawn area
---@param size_mod number
---@param amount_mod number
---@return nil
function PlaceResourcesInSquare(surface, position, size_mod, amount_mod)
-- Create list of resource tiles
---@type table<string>
local r_list = {}
for r_name, _ in pairs(storage.ocfg.surfaces_config[surface.name].spawn_config.solid_resources --[[@as table<string, OarcConfigSolidResource>]]) do
if (r_name ~= "") then
table.insert(r_list, r_name)
end
end
---@type table<string>
local shuffled_list = FYShuffle(r_list)
local spawn_general = storage.ocfg.spawn_general
local spawn_config = storage.ocfg.surfaces_config[surface.name].spawn_config
local radius = spawn_general.spawn_radius_tiles * spawn_config.radius_modifier
-- Get the top left position of the spawn area
local resource_position = { x = position.x - radius,
y = position.y - radius }
-- Offset the starting position
resource_position.x = resource_position.x + storage.ocfg.resource_placement.horizontal_offset
resource_position.y = resource_position.y + storage.ocfg.resource_placement.vertical_offset
-- Place vertically using linear spacing
for _, r_name in pairs(shuffled_list) do
local resourceConfig = storage.ocfg.surfaces_config[surface.name].spawn_config.solid_resources[r_name]
local size = resourceConfig.size * size_mod
GenerateResourcePatch(surface, r_name, size, resource_position, resourceConfig.amount * amount_mod)
resource_position.y = resource_position.y + size + storage.ocfg.resource_placement.linear_spacing
end
end
---Places the final spawn elements after chunk generation is complete.
---@param delayed_spawn OarcDelayedSpawn
---@return nil
function GenerateFinalSpawnPieces(delayed_spawn)
local ocfg --[[@as OarcConfig]] = storage.ocfg
local spawn_config = ocfg.surfaces_config[delayed_spawn.surface_name].spawn_config
local surface = game.surfaces[delayed_spawn.surface_name]
-- DOUBLE CHECK and make sure the area is super safe.
ClearNearbyEnemies(delayed_spawn.position, spawn_config.safe_area.safe_radius * CHUNK_SIZE,
game.surfaces[delayed_spawn.surface_name])
-- Generate water strip only if we don't have a moat.
if (not delayed_spawn.moat or spawn_config.liquid_tile == "lava") then
GenerateStartingLiquidStrip(delayed_spawn, spawn_config, surface)
end
-- Create the spawn resources here
GenerateStartingResources(surface, delayed_spawn.position)
local radius = storage.ocfg.spawn_general.spawn_radius_tiles * spawn_config.radius_modifier
-- Reference position is RIGHT (WEST) of the spawn area.
local sharing_ref_pos = {
x = delayed_spawn.position.x + radius,
y = delayed_spawn.position.y
}
-- Create shared power poles
if (ocfg.gameplay.enable_shared_power and delayed_spawn.primary) then
local power_pole_position = {
x = sharing_ref_pos.x + spawn_config.shared_power_pole_position.x_offset,
y = sharing_ref_pos.y + spawn_config.shared_power_pole_position.y_offset }
CreateSharedPowerPolePair(surface, power_pole_position)
end
-- Create shared chest
if (ocfg.gameplay.enable_shared_chest and delayed_spawn.primary) then
local chest_position = {
x = sharing_ref_pos.x + spawn_config.shared_chest_position.x_offset,
y = sharing_ref_pos.y + spawn_config.shared_chest_position.y_offset }
CreateSharedChest(surface, chest_position)
end
-- Place randomized entities
if (delayed_spawn.surface_name == "fulgora") then
PlaceFulgoranLightningAttractors(surface, delayed_spawn.position, 10)
end
PlaceRandomEntities(surface, delayed_spawn.position)
-- Only primary spawns get a crashed ship.
if delayed_spawn.primary then
-- Create crash site if configured
if (ocfg.surfaces_config[delayed_spawn.surface_name].starting_items.crashed_ship) then
crash_site.create_crash_site(surface,
{ x = delayed_spawn.position.x + 15, y = delayed_spawn.position.y - 25 },
ocfg.surfaces_config[delayed_spawn.surface_name].starting_items.crashed_ship_resources,
ocfg.surfaces_config[delayed_spawn.surface_name].starting_items.crashed_ship_wreakage)
end
end
-- Render some welcoming text...
DisplayWelcomeGroundTextAtSpawn(delayed_spawn.surface_name, delayed_spawn.position)
-- Trigger the event that the spawn was created.
script.raise_event("oarc-mod-on-spawn-created", {spawn_data = storage.unique_spawns[delayed_spawn.surface_name][delayed_spawn.host_name]})
end
---Sends the player to a NEW spawn point (primary OR secondary)
---@param player_name string
---@param surface_name string
---@param first_spawn boolean
---@param is_host boolean
---@return nil
function SendPlayerToNewSpawn(player_name, surface_name, first_spawn, is_host)
local player = game.players[player_name]
-- Check if player character is nil
if (player.character == nil) then
log("Player character is nil, can't send to spawn point just yet: " .. player_name)
QueueNilCharacterForNewSpawnTeleport(player_name, surface_name, first_spawn, is_host)
return
end
-- Send the player to that position
TeleportPlayerToRespawnPoint(surface_name, player, first_spawn)
-- Remove waiting dialog
if (player.gui.screen.wait_for_spawn_dialog ~= nil) then
player.gui.screen.wait_for_spawn_dialog.destroy()
end
-- Only first time spawns get starter items.
if first_spawn then
GivePlayerStarterItems(player)
end
-- Trigger the event that player was spawned too.
script.raise_event("oarc-mod-on-player-spawned", {player_index = player.index, first_spawn = first_spawn, is_host = is_host})
end
---Displays some welcoming text at the spawn point on the ground. Fades out over time.
---@param surface LuaSurface|string
---@param position MapPosition
---@return nil
function DisplayWelcomeGroundTextAtSpawn(surface, position)
-- Render some welcoming text...
local tcolor = { 0.9, 0.7, 0.3, 0.8 }
local ttl = 2000
local render_object_1 = rendering.draw_text { text = {"oarc-spawn-ground-text-welcome"},
surface = surface,
target = { x = position.x - 21, y = position.y - 15 },
color = tcolor,
scale = 20,
font = "compi",
time_to_live = ttl,
draw_on_ground = true,
orientation = 0,
-- alignment=center,
scale_with_zoom = false,
only_in_alt_mode = false }
local render_object_2 = rendering.draw_text { text = {"oarc-spawn-ground-text-home"},
surface = surface,
target = { x = position.x - 14, y = position.y - 5 },
color = tcolor,
scale = 20,
font = "compi",
time_to_live = ttl,
draw_on_ground = true,
orientation = 0,
-- alignment=center,
scale_with_zoom = false,
only_in_alt_mode = false }
local rid1 = render_object_1.id
local rid2 = render_object_2.id
table.insert(storage.oarc_renders_fadeout, rid1)
table.insert(storage.oarc_renders_fadeout, rid2)
end
--[[
___ _ _ _ _ _ _ _ __ ___ ___ _ _ ___ ___ _ _____ ___ ___ _ _
/ __|| || || | | || \| || |/ / / __|| __|| \| || __|| _ \ /_\|_ _||_ _|/ _ \ | \| |
| (__ | __ || |_| || .` || ' < | (_ || _| | .` || _| | / / _ \ | | | || (_) || .` |
\___||_||_| \___/ |_|\_||_|\_\ \___||___||_|\_||___||_|_\/_/ \_\|_| |___|\___/ |_|\_|
--]]
---Clear the spawn areas. This should be run inside the chunk generate event and be given a list of all
---unique spawn points. This clears enemies in the immediate area, creates a slightly safe area around it,
---Resources are generated at a delayed time when the player is moved to the spawn point! It only works off of
---the closest spawn point!!
---@param surface LuaSurface
---@param chunkArea BoundingBox
---@return nil
function SetupAndClearSpawnAreas(surface, chunkArea)
local closest_spawn = GetClosestUniqueSpawn(surface.name, chunkArea.left_top)
if (closest_spawn == nil) then return end
--[[@type OarcConfigSpawnGeneral]]
local general_spawn_config = storage.ocfg.spawn_general
local surface_spawn_config = storage.ocfg.surfaces_config[surface.name].spawn_config
local radius = general_spawn_config.spawn_radius_tiles * surface_spawn_config.radius_modifier
local chunkAreaCenter = {
x = chunkArea.left_top.x + (CHUNK_SIZE / 2),
y = chunkArea.left_top.y + (CHUNK_SIZE / 2)
}
-- If there is a buddy spawn, we need to setup both areas TOGETHER so they overlap.
local spawns = { closest_spawn }
if (closest_spawn.buddy_name ~= nil) then
table.insert(spawns, storage.unique_spawns[closest_spawn.surface_name][closest_spawn.buddy_name])
end
-- This will typically just contain the one spawn point, but if there is a buddy spawn, it will contain both.
for _, spawn in pairs(spawns) do
-- If the chunk is within the main land area, then clear trees/resources and create the land spawn areas
-- (guaranteed land with a circle of trees)
local landArea = GetAreaAroundPos(spawn.position, radius + CHUNK_SIZE)
if not CheckIfInArea(chunkAreaCenter, landArea) then
goto CONTINUE
end
-- Remove trees/resources inside the spawn area
if (general_spawn_config.shape == SPAWN_SHAPE_CHOICE_CIRCLE) or (general_spawn_config.shape == SPAWN_SHAPE_CHOICE_OCTAGON) then
RemoveInCircle(surface, chunkArea, {"resource", "cliff", "tree", "plant", "lightning-attractor", "simple-entity"}, spawn.position, radius + 5)
elseif (general_spawn_config.shape == SPAWN_SHAPE_CHOICE_SQUARE) then
RemoveInSquare(surface, chunkArea, {"resource", "cliff", "tree", "plant", "lightning-attractor", "simple-entity"}, spawn.position, radius + 5)
end
if (general_spawn_config.shape == SPAWN_SHAPE_CHOICE_CIRCLE) then
CreateCropCircle(surface, spawn, chunkArea)
elseif (general_spawn_config.shape == SPAWN_SHAPE_CHOICE_OCTAGON) then
CreateCropOctagon(surface, spawn, chunkArea)
elseif (general_spawn_config.shape == SPAWN_SHAPE_CHOICE_SQUARE) then
CreateCropSquare(surface, spawn, chunkArea)
end
script.raise_event("oarc-mod-on-chunk-generated-near-spawn", {surface = surface, chunk_area = chunkArea, spawn_data = spawn})
:: CONTINUE ::
end
end
---This is the main function that creates the spawn area. Provides resources, land and a safe zone.
---@param event EventData.on_chunk_generated
---@return nil
function SeparateSpawnsGenerateChunk(event)
local surface = event.surface
local chunkArea = event.area
-- Don't block based on spawn enabled.
-- if (not storage.oarc_surfaces[surface.name]) then return end
-- Downgrade resources near to spawns
-- TODO: Space Age will change this!
if storage.ocfg.gameplay.scale_resources_around_spawns then
DowngradeResourcesDistanceBasedOnChunkGenerate(surface, chunkArea)
end
-- This handles chunk generation near player spawns
-- If it is near a player spawn, it provide a guaranteed area of land and water tiles.
SetupAndClearSpawnAreas(surface, chunkArea)
end
---Based on the danger distance, you get full resources, and it is exponential from the spawn point to that distance.
---@param surface LuaSurface
---@param chunkArea BoundingBox
---@return nil
function DowngradeResourcesDistanceBasedOnChunkGenerate(surface, chunkArea)
local closestSpawn = GetClosestUniqueSpawn(surface.name, chunkArea.left_top)
if (closestSpawn == nil) then return end
local distance = util.distance(chunkArea.left_top, closestSpawn.position)
-- Adjust multiplier to bring it in or out
local modifier = (distance / (storage.ocfg.surfaces_config[surface.name].spawn_config.safe_area.danger_radius * CHUNK_SIZE * 1)) ^ 3
if modifier < 0.1 then modifier = 0.1 end
if modifier > 1 then return end
local ore_per_tile_cap = math.floor(100000 * modifier)
for _, entity in pairs(surface.find_entities_filtered { area = chunkArea, type = "resource" }) do
if entity.valid and entity.amount then
local new_amount = math.ceil(entity.amount * modifier)
if (new_amount < 1) then
entity.destroy()
else
if (entity.prototype.resource_category ~= "basic-fluid") then
entity.amount = math.min(new_amount, ore_per_tile_cap)
else
entity.amount = new_amount
end
end
end
end
end
--[[
___ _ ___ _ _ _ _ _ ___
/ __|| | | __| /_\ | \| || | | || _ \
| (__ | |__ | _| / _ \ | .` || |_| || _/
\___||____||___|/_/ \_\|_|\_| \___/ |_|
--]]
---Call this if a player leaves the game early (or a player wants an early game reset)
---@param player LuaPlayer
---@param remove_player boolean Deletes player from the game assuming they are offline.
function RemoveOrResetPlayer(player, remove_player)
if (not player) then
log("ERROR - CleanupPlayer on NIL Player!")
return
end
-- If playtime is less than minimum online time, try to remove starter items
if (player.online_time < (storage.ocfg.gameplay.minimum_online_time * TICKS_PER_MINUTE)) then
RemovePlayerStarterItems(player)
end
-- If this player is staying in the game, lets make sure we don't delete them along with the map chunks being
-- cleared.
SafeTeleport(player, game.surfaces[HOLDING_PEN_SURFACE_NAME], {x=0,y=0})
local player_old_force = player.force
player.force = storage.ocfg.gameplay.main_force_name
local player_name = player.name
-- Clear globals and transfers spawn ownership if needed.
CleanupPlayerGlobals(player_name) -- This cleans storage.unique_spawns IF we are transferring ownership.
-- Safely clear all spawns if they are the host.
local spawns = FindAllUniqueSpawnsWherePlayerIsTheHost(player_name)
for _,spawn in pairs(spawns) do
UniqueSpawnCleanupRemove(player_name, spawn)
end
-- Remove a force if this player created it and they are the only one on it
if ((#player_old_force.players == 0) and (player_old_force.name ~= storage.ocfg.gameplay.main_force_name)) then
log("RemoveOrResetPlayer - FORCE REMOVED: " .. player_old_force.name)
game.merge_forces(player_old_force, "neutral")
end
-- Trigger the event that the player was reset.
script.raise_event("oarc-mod-on-player-reset", {player_index = player.index})
-- Remove the character completely
if (remove_player and not player.connected) then
game.remove_offline_players({ player }) -- SOMEHOW RELATED TO THE DESYNCS? (This plus cheat command for other planets.)
-- Otherwise, make sure to re-init them!
else
if (remove_player) then
log("ERROR! RemoveOrResetPlayer - Player not removed as they are still connected: " .. player_name)
end
SeparateSpawnsInitPlayer(player.index --[[@as string]])
SendBroadcastMsg({"oarc-player-was-reset-notify", player_name})
end
-- Refresh the shared spawn spawn gui for all players
for _,p in pairs(game.connected_players) do
RefreshSharedSpawnFrameIfExist(p)
end
end
---Searches all unique spawns for the primary one for a player. This will return null if they joined someeone else's spawn.
---@param player_name string
---@return OarcUniqueSpawn?
function FindPrimaryUniqueSpawn(player_name)
for _,spawns in pairs(storage.unique_spawns) do
if (spawns[player_name] ~= nil and spawns[player_name].primary) then
return spawns[player_name]
end
end
return nil
end
---Returns all spawns (primary and secondary) where this player is the host
---@param player_name string
---@return OarcUniqueSpawn[]
function FindAllUniqueSpawnsWherePlayerIsTheHost(player_name)
local found_spawns = {}
for _,spawns in pairs(storage.unique_spawns) do
for _, spawn in pairs(spawns) do
if (spawn.host_name == player_name) then
table.insert(found_spawns, spawn)
end
end
end
return found_spawns
end
---Find the primary home spawn of a player, if one exists. This will return a shared spawn if they joined one.
---@param player_name string
---@return OarcUniqueSpawn?
function FindPlayerHomeSpawn(player_name)
for _,spawns in pairs(storage.unique_spawns) do
for _,spawn in pairs(spawns) do
if (spawn.primary) and ((spawn.host_name == player_name) or TableContains(spawn.joiners, player_name)) then
return spawn
end
end
end
end
---Get the spawn choices from whoever is the host of the spawn the player joined, even if they are the host.
---@param player_name string
---@return OarcSpawnChoices?
function GetPrimarySpawnChoices(player_name)
local primary_spawn = FindPlayerHomeSpawn(player_name)
if (primary_spawn == nil) then return nil end
return storage.spawn_choices[primary_spawn.host_name]
end
---Find the spawn of a player, if one exists, on a specific surface.
---@param player_name string
---@param surface_name string
---@return OarcUniqueSpawn?
function FindPlayerSpawnOnSurface(player_name, surface_name)
if (storage.unique_spawns[surface_name] == nil) then return nil end
for _,spawn in pairs(storage.unique_spawns[surface_name]) do
if ((spawn.host_name == player_name) or TableContains(spawn.joiners, player_name)) then
return spawn
end
end
end
---Searches all unique spawns for a list of secondary ones for a player.
---@param player_name string
---@return table<string, OarcUniqueSpawn> -- Indexed by surface name!
function FindSecondaryUniqueSpawns(player_name)
local secondary_spawns = {}
for surface_index, spawns in pairs(storage.unique_spawns) do
if (spawns[player_name] ~= nil and not spawns[player_name].primary) then
secondary_spawns[surface_index] = spawns[player_name]
end
end
return secondary_spawns
end
---Searches through the delayed spawns to see if there is one for this player.
---@param player_name string
---@param surface_name string
---@return OarcDelayedSpawn?
function FindDelayedSpawn(player_name, surface_name)
for _,delayed_spawn in pairs(storage.delayed_spawns) do
if (delayed_spawn.surface_name == surface_name) then
if (delayed_spawn.host_name == player_name) or (TableContains(delayed_spawn.joiners, player_name)) then
return delayed_spawn
end
end
end
end
---Cleans up a player's unique spawn point, if safe to do so.
---@param player_name string
---@param unique_spawn OarcUniqueSpawn?
---@return nil
function UniqueSpawnCleanupRemove(player_name, unique_spawn)
if (unique_spawn == nil) then return end -- Safety
log("UniqueSpawnCleanupRemove - " .. player_name .. " on surface: " .. unique_spawn.surface_name)
local spawn_config = storage.ocfg.surfaces_config[unique_spawn.surface_name].spawn_config
local total_spawn_width = (storage.ocfg.spawn_general.spawn_radius_tiles * spawn_config.radius_modifier) +
storage.ocfg.spawn_general.moat_width_tiles
-- Check if it was near someone else's base. (Really just buddy base is possible I think?)
nearOtherSpawn = false
for player_index, spawn in pairs(storage.unique_spawns[unique_spawn.surface_name]) do
if ((player_index ~= player_name) and
(util.distance(unique_spawn.position, spawn.position) < (total_spawn_width * 3))) then
log("Won't remove base as it's close to another spawn: " .. player_index)
nearOtherSpawn = true
end
end
-- TODO: Possibly limit this based on playtime? If player is on for a long time then don't remove it?
-- Use regrowth mod to cleanup the area.
local spawn_position = unique_spawn.position
if (storage.ocfg.regrowth.enable_abandoned_base_cleanup and (not nearOtherSpawn)) then
log("Removing base: " .. spawn_position.x .. "," .. spawn_position.y .. " on surface: " .. unique_spawn.surface_name)
-- Clear an area around the spawn that SHOULD not include any other bases.
local clear_radius = storage.ocfg.gameplay.minimum_distance_to_existing_chunks - 2 -- Bring in a bit for safety.
RegrowthMarkAreaForRemoval(unique_spawn.surface_name, spawn_position, clear_radius)
TriggerCleanup()
-- Trigger event
script.raise_event("oarc-mod-on-spawn-remove-request", {spawn_data = unique_spawn})
end
-- Remove the spawn point from the global table.
storage.unique_spawns[unique_spawn.surface_name][player_name] = nil
end
---Cleans up all references to a player in the global tables.
---@param player_name string
---@return nil
function CleanupPlayerGlobals(player_name)
-- Clear the buddy pair IF one exists
if (storage.buddy_pairs[player_name] ~= nil) then
local buddy_name = storage.buddy_pairs[player_name]
storage.buddy_pairs[player_name] = nil
storage.buddy_pairs[buddy_name] = nil
-- Nil the buddy from any of the unique spawn entries
for _,spawns in pairs(storage.unique_spawns) do
if (spawns[buddy_name] ~= nil) then
spawns[buddy_name].buddy_name = nil
end
end
end
-- Transfer or remove a shared spawn if player is owner
local unique_spawn = FindPrimaryUniqueSpawn(player_name)
if (unique_spawn ~= nil and #unique_spawn.joiners > 0) then
local new_owner_name = table.remove(unique_spawn.joiners) -- Get 1 to use as new owner.
TransferOwnershipOfAllSpawns(unique_spawn, new_owner_name)
SendBroadcastMsg( {"oarc-host-left-new-host", player_name, new_owner_name})
end
-- Check all other shared spawns too in case they joined one.
for surface_index, spawns in pairs(storage.unique_spawns) do
for player_index, spawn in pairs(spawns) do
for index, joiner in pairs(spawn.joiners) do
if (player_name == joiner) then
storage.unique_spawns[surface_index][player_index].joiners[index] = nil
end
end
end
end
-- Clear their personal spawn point info
if (storage.player_respawns[player_name] ~= nil) then
storage.player_respawns[player_name] = nil
end
-- Remove them from the delayed spawn queue if they are still the host
for index, delayedSpawn in pairs(storage.delayed_spawns --[[@as OarcDelayedSpawnsTable]]) do
if (player_name == delayedSpawn.host_name) then
storage.delayed_spawns[index] = nil
log("Removing player from delayed spawn queue: " .. player_name)
break
end
end
-- Remove them from any join queues they may be in:
RemovePlayerFromJoinQueue(player_name)
if (storage.player_cooldowns[player_name] ~= nil) then
storage.player_cooldowns[player_name] = nil
end
end
---Transfers ownership of a shared spawn to another player.
---@param primary_spawn OarcUniqueSpawn
---@param new_host_name string
---@return nil
function TransferOwnershipOfAllSpawns(primary_spawn, new_host_name)
-- Transfer every primary AND secondary spawn.
for surface_name, unique_spawns in pairs(storage.unique_spawns) do
for host_name, unique_spawn in pairs(unique_spawns) do
if (host_name == primary_spawn.host_name) then
-- Create a new unique for the new owner based on the old one.
storage.unique_spawns[surface_name][new_host_name] = table.deepcopy(unique_spawn)
-- local new_spawn = table.deepcopy(unique_spawn)
storage.unique_spawns[surface_name][new_host_name].host_name = new_host_name
storage.unique_spawns[surface_name][new_host_name].joiners = table.deepcopy(primary_spawn.joiners) -- Copy the old joiner list, with the new host already removed.
-- Update the matching buddy spawn if it exists.
if (primary_spawn.buddy_name ~= nil) then
storage.unique_spawns[surface_name][unique_spawn.buddy_name].buddy_name = new_host_name
end
-- Remove the old spawn
storage.unique_spawns[surface_name][host_name] = nil
end
end
end
-- Transfer any delayed_spawns
for index, delayed_spawn in pairs(storage.delayed_spawns) do
if (delayed_spawn.host_name == primary_spawn.host_name) then
storage.delayed_spawns[index].host_name = new_host_name
end
end
game.players[new_host_name].print({ "oarc-new-owner-msg" })
end
--[[
_ _ ___ _ ___ ___ ___ ___ _____ _ _ ___ ___
| || || __|| | | _ \| __|| _ \ / __||_ _|| | | || __|| __|
| __ || _| | |__ | _/| _| | / \__ \ | | | |_| || _| | _|
|_||_||___||____||_| |___||_|_\ |___/ |_| \___/ |_| |_|
--]]
---Queue a player whose character is nil when being sent to a new spawn.
---A separate on_tick function will read this queue and send the player to the spawn point when they have a character again.
---@param player_name string
---@param surface_name string
---@param first_spawn boolean
---@param is_host boolean
---@return nil
function QueueNilCharacterForNewSpawnTeleport(player_name, surface_name, first_spawn, is_host)
if (storage.nil_character_teleport_queue[player_name] == nil) then
storage.nil_character_teleport_queue[player_name] = { surface_name = surface_name, first_spawn = first_spawn, is_host = is_host }
end
end
---On tick function to process the nil character teleport queue.
---@return nil
function OnTickNilCharacterTeleportQueue()
for player_name, data in pairs(storage.nil_character_teleport_queue) do
local player = game.players[player_name]
-- If player is nil, remove them from the queue.
if (player == nil) then
storage.nil_character_teleport_queue[player_name] = nil
-- Else if they have a character, send them to the spawn point.
-- And hope to high heaven this doesn't recurse infinitely.
elseif (player.character ~= nil) then
storage.nil_character_teleport_queue[player_name] = nil
SendPlayerToNewSpawn(player_name, data.surface_name, data.first_spawn, data.is_host)
end
end
end
---Finds and removes a player from a shared spawn join queue, and refreshes the host's GUI.
---@param player_name string
---@return boolean
function RemovePlayerFromJoinQueue(player_name)
for surface_index, spawns in pairs(storage.unique_spawns) do
for player_index, spawn in pairs(spawns) do
for index, requestor in pairs(spawn.join_queue) do
if (requestor == player_name) then
storage.unique_spawns[surface_index][player_index].join_queue[index] = nil
local host_player = game.players[player_index]
if (host_player ~= nil) and (host_player.connected) then
OarcGuiRefreshContent(host_player)
end
return true
end
end
end
end
return false
end
---Same as GetClosestPosFromTable but specific to storage.unique_spawns
---@param surface_name string
---@param pos MapPosition
---@return OarcUniqueSpawn?
function GetClosestUniqueSpawn(surface_name, pos)
local surface_spawns = storage.unique_spawns[surface_name]
if (surface_spawns == nil) then return nil end -- EXIT - No spawns on requested surface
if (table_size(surface_spawns) == 0) then return nil end -- EXIT - No spawns on requested surface
local closest_dist = nil
local closest_spawn = nil
for _,spawn in pairs(surface_spawns) do
local new_dist = util.distance(pos, spawn.position)
if (closest_dist == nil) then
closest_dist = new_dist
closest_spawn = spawn
elseif (closest_dist > new_dist) then
closest_dist = new_dist
closest_spawn = spawn
end
end
return closest_spawn
end
---Find all players that belong to the same PRIMARY shared spawn as this player, including buddies!
---@param player_name string
---@param include_offline boolean
---@return string[]
function GetPlayersFromSameSpawn(player_name, include_offline)
local shared_players = {}
for _, spawns in pairs(storage.unique_spawns) do
for _,spawn in pairs(spawns) do
if (not spawn.primary) then goto CONTINUE end
-- Is the player either the host OR a joiner OR a buddy?
if (spawn.host_name == player_name) or (TableContains(spawn.joiners, player_name) or (spawn.buddy_name == player_name)) then
if (include_offline or game.players[spawn.host_name].connected) then
table.insert(shared_players, spawn.host_name)
end
for _,joiner in pairs(spawn.joiners) do
if (include_offline or game.players[joiner].connected) then
table.insert(shared_players, joiner)
end
end
if (spawn.buddy_name ~= nil) then
if (include_offline or game.players[spawn.buddy_name].connected) then
table.insert(shared_players, spawn.buddy_name)
end
end
return shared_players -- We only need to find one match.
end
:: CONTINUE ::
end
end
return shared_players
end
---Returns the number of players currently online at the shared spawn including the host.
---@param surface_name string
---@param owner_name string
---@return number
function GetOnlinePlayersAtSharedSpawn(surface_name, owner_name)
local spawn = storage.unique_spawns[surface_name][owner_name]
if spawn == nil then return 0 end
-- Does not count base owner
local count = 0
-- For each player in the shared spawn, check if online and add to count.
for _,joiner in pairs(spawn.joiners) do
if game.players[joiner].connected then
count = count + 1
end
end
-- Add the host player to the count
if game.players[owner_name].connected then
count = count + 1
end
return count
end
-- -- Get the number of currently available shared spawns.
-- -- This means the base owner has enabled access AND the number of online players
-- -- is below the threshold.
-- ---@return number
-- function GetNumberOfAvailableSharedSpawns()
-- return #GetAvailableSharedSpawns()
-- end
---This is used to provide both a list of spawns and a list of hosts for easy display in the GUI.
---@alias AvailableSpawnsTable { hosts: string[], spawns : OarcUniqueSpawn[] }
---Get a list of available shared spawns.
---@return AvailableSpawnsTable
function GetAvailableSharedSpawns()
local available_spawns = { hosts = {}, spawns = {} }
for surface_index, spawns in pairs(storage.unique_spawns) do
for owner_name, spawn in pairs(spawns) do
if IsSharedSpawnOpen(surface_index, owner_name) and not IsSharedSpawnFull(surface_index, owner_name) then
table.insert(available_spawns.hosts, owner_name)
table.insert(available_spawns.spawns, spawn)
end
end
end
return available_spawns
end
---Check if a specific shared spawn is valid, open and host is online (might still be full!)
---@param surface_name string
---@param owner_name string
---@return boolean
function IsSharedSpawnOpen(surface_name, owner_name)
if (storage.unique_spawns[surface_name] == nil) or (storage.unique_spawns[surface_name][owner_name] == nil) then
return false
end
local spawn = storage.unique_spawns[surface_name][owner_name]
if (not spawn.open_access) then
return false
end
-- Currently don't support directly joining secondary spawns!
if (not spawn.primary) then
return false
end
if (game.players[owner_name] == nil) or not (game.players[owner_name].connected) then
return false
end
return true
end
---Check if a specific shared spawn is full.
---@param surface_name string
---@param owner_name string
---@return boolean --True if the shared spawn is full or invalid.
function IsSharedSpawnFull(surface_name, owner_name)
if (storage.unique_spawns[surface_name][owner_name] == nil) then return true end
-- Technically I only limit the players based on if they are online, so you can exceed the limit if players join
-- while others are offline. This is a feature, not a bug?
return (GetOnlinePlayersAtSharedSpawn(surface_name, owner_name) >= storage.ocfg.gameplay.number_of_players_per_shared_spawn)
end
---Creates a new spawn, this triggers the chunk generation and will provide an event when done.
---@param host_name string
---@param surface_name string -- Might be different from spawn_choices if it is a secondary spawn.
---@param spawn_position MapPosition
---@param spawn_choices OarcSpawnChoices
---@param primary boolean
---@return OarcDelayedSpawn
function GenerateNewSpawn(host_name, surface_name, spawn_position, spawn_choices, primary)
-- Double check...
if ((spawn_position.x == 0) and (spawn_position.y == 0)) then
error("Invalid spawn position for GenerateNewSpawn: " .. host_name .. " on surface: " .. spawn_choices.surface_name)
end
local unique_spawn = nil
if primary then
unique_spawn = InitPrimarySpawnGlobals(host_name, spawn_position, spawn_choices)
else
local primary_spawn = FindPrimaryUniqueSpawn(host_name)
if (primary_spawn == nil) then
error("ERROR - GenerateNewSpawn - No primary spawn found for: " .. host_name)
end
unique_spawn = InitSecondarySpawnGlobals(primary_spawn, surface_name, spawn_position)
end
return QueueNewSpawnGeneration(unique_spawn)
end
---Queues a player who is waiting on their spawn to be generated.
---@param player_name string
---@param delayed_spawn OarcDelayedSpawn
---@return nil
function QueuePlayerForSpawn(player_name, delayed_spawn)
-- Send them to the holding pen if they are not already there.
local player = game.players[player_name]
if (player.surface.name ~= HOLDING_PEN_SURFACE_NAME) then
SafeTeleport(player, game.surfaces[HOLDING_PEN_SURFACE_NAME], {x=0,y=0})
end
SetPlayerRespawn(player_name, delayed_spawn.surface_name, delayed_spawn.position, true)
game.players[player_name].print({ "oarc-generating-spawn-please-wait" })
local ticks_remaining = delayed_spawn.delayed_tick - game.tick
local seconds_remaining = math.ceil(ticks_remaining / TICKS_PER_SECOND)
HideOarcGui(game.players[player_name])
DisplayPleaseWaitForSpawnDialog(game.players[player_name], seconds_remaining, game.surfaces[delayed_spawn.surface_name], delayed_spawn.position)
table.insert(delayed_spawn.waiting_players, player_name)
log("QueuePlayerForSpawn - Player:" .. player_name .. " - Host:" .. delayed_spawn.host_name)
end
---Sets the custom spawn point for a player. They can have one per surface.
---@param player_name string
---@param surface_name string
---@param position MapPosition
---@param reset_cooldown boolean
---@return nil
function SetPlayerRespawn(player_name, surface_name, position, reset_cooldown)
---@type OarcPlayerSpawn
local updatedPlayerSpawn = {}
updatedPlayerSpawn.surface = surface_name
updatedPlayerSpawn.position = position
storage.player_respawns[player_name][surface_name] = updatedPlayerSpawn
if (storage.player_cooldowns[player_name].setRespawn == nil) or reset_cooldown then
storage.player_cooldowns[player_name].setRespawn = game.tick
end
end
---Creates the storage.unique_spawns entries for a new PRIMARY spawn area.
---@param host_name string
---@param spawn_position MapPosition
---@param spawn_choices OarcSpawnChoices
---@return OarcUniqueSpawn
function InitPrimarySpawnGlobals(host_name, spawn_position, spawn_choices)
---@type OarcUniqueSpawn
local new_unique_spawn = {
surface_name = spawn_choices.surface_name,
position = spawn_position,
moat = spawn_choices.moat,
primary = true,
host_name = host_name,
joiners = {},
join_queue = {},
open_access = false,
buddy_name = spawn_choices.buddy,
generated = false
}
if storage.unique_spawns[spawn_choices.surface_name] == nil then
storage.unique_spawns[spawn_choices.surface_name] = {}
end
storage.unique_spawns[spawn_choices.surface_name][host_name] = new_unique_spawn
return new_unique_spawn
end
---Creates the storage.unique_spawns entries for a new SECONDARY spawn area.
---@param unique_spawn OarcUniqueSpawn
---@param surface_name string
---@param spawn_position MapPosition
---@return OarcUniqueSpawn
function InitSecondarySpawnGlobals(unique_spawn, surface_name, spawn_position)
local new_unique_spawn = table.deepcopy(unique_spawn)
new_unique_spawn.surface_name = surface_name
new_unique_spawn.position = spawn_position
new_unique_spawn.primary = false
new_unique_spawn.generated = false
if storage.unique_spawns[surface_name] == nil then
storage.unique_spawns[surface_name] = {}
end
storage.unique_spawns[surface_name][unique_spawn.host_name] = new_unique_spawn
return new_unique_spawn
end
---Starts the generation of a new unique spawn. This will generate an event when done.
---@param unique_spawn OarcUniqueSpawn
---@return OarcDelayedSpawn
function QueueNewSpawnGeneration(unique_spawn)
local spawn_config = storage.ocfg.surfaces_config[unique_spawn.surface_name].spawn_config
local radius = storage.ocfg.spawn_general.spawn_radius_tiles * spawn_config.radius_modifier
local total_spawn_width = radius + storage.ocfg.spawn_general.moat_width_tiles
local spawn_chunk_radius = math.ceil(total_spawn_width / CHUNK_SIZE) + 1 -- Add a 1 chunk buffer to be safe
-- This is just a rough estimate of worst case chunk generation time.
-- If we hit this timeout, usually it means something has gone wrong.
local delay_spawn_seconds = 5 * spawn_chunk_radius
-- Trigger the chunk generation
game.surfaces[unique_spawn.surface_name].request_to_generate_chunks(unique_spawn.position, spawn_chunk_radius)
local final_chunk = GetChunkPosFromTilePos(unique_spawn.position)
final_chunk.x = final_chunk.x + spawn_chunk_radius
final_chunk.y = final_chunk.y + spawn_chunk_radius
---@type OarcDelayedSpawn
local delayed_spawn = {
-- I do this explicitly so that I get the LUA warnings if I miss a field!
-- I know I could use table.deepcopy, but this is INTENTIONAL!
surface_name = unique_spawn.surface_name,
position = unique_spawn.position,
moat = unique_spawn.moat,
primary = unique_spawn.primary,
host_name = unique_spawn.host_name,
joiners = unique_spawn.joiners,
join_queue = unique_spawn.join_queue,
open_access = unique_spawn.open_access,
buddy_name = unique_spawn.buddy_name,
-- This is the extra data I need for the delayed spawn
delayed_tick = game.tick + delay_spawn_seconds * TICKS_PER_SECOND,
final_chunk_generated = final_chunk,
waiting_players = {}
}
table.insert(storage.delayed_spawns, delayed_spawn)
RegrowthMarkAreaSafeGivenTilePos(unique_spawn.surface_name, unique_spawn.position, spawn_chunk_radius, true)
-- Chart the area to be able to display the minimap while the player waits.
ChartArea(game.players[unique_spawn.host_name].force,
delayed_spawn.position,
spawn_chunk_radius,
unique_spawn.surface_name)
return delayed_spawn
end
---Creates and sends a player to a new secondary spawn, temporarily placing them in the holding pen.
---@param player LuaPlayer
---@param surface_name string
---@return nil
function SecondarySpawn(player, surface_name)
local player_name = player.name
-- Get their home spawn first:
local primary_spawn = FindPlayerHomeSpawn(player_name)
local host_name = primary_spawn.host_name
-- Ensure we still have the previous spawn choices (theirs or the host's)
local spawn_choices = GetPrimarySpawnChoices(host_name)
if (spawn_choices == nil) then
log("ERROR - SecondarySpawn - No spawn choices for player: " .. host_name)
return
end
-- Confirm there is no existing spawn point for this host on this surface
if (storage.unique_spawns[surface_name] ~= nil and storage.unique_spawns[surface_name][host_name] ~= nil) then
log("ERROR - SecondarySpawn - Host already has a spawn point on this surface: " .. host_name .. " on surface: " .. surface_name)
return
end
-- Find a new spawn point
local spawn_position = FindUngeneratedCoordinates(surface_name, spawn_choices.distance, 3)
-- If that fails, just throw a warning and don't spawn them. They can try again.
if ((spawn_position.x == 0) and (spawn_position.y == 0)) then
SendErrorMsg(player, { "oarc-no-ungenerated-land-error" })
return
end
-- Add new spawn point for the new surface
local delayed_spawn = GenerateNewSpawn(host_name, surface_name, spawn_position, spawn_choices, false)
QueuePlayerForSpawn(player_name, delayed_spawn)
-- Handle special buddy spawns:
if (spawn_choices.buddy) then
local buddy_position = GetBuddySpawnPosition(spawn_position, surface_name, spawn_choices.moat)
local buddy_choices = storage.spawn_choices[spawn_choices.buddy]
GenerateNewSpawn(spawn_choices.buddy, surface_name, buddy_position, buddy_choices, false)
SetPlayerRespawn(spawn_choices.buddy, surface_name, buddy_position, false)
-- Make sure host and joiners all have their new respawn position set for this surface.
elseif (#storage.unique_spawns[surface_name][host_name].joiners > 0) then
SetPlayerRespawn(host_name, surface_name, spawn_position, false)
for _,joiner_name in pairs(storage.unique_spawns[surface_name][host_name].joiners) do
SetPlayerRespawn(joiner_name, surface_name, spawn_position, false)
end
end
-- Announce
SendBroadcastMsg({"", { "oarc-player-new-secondary", player_name, surface_name }, " ", GetGPStext(surface_name, spawn_position)})
-- Tell the player about the reroll command:
CompatSend(player, { "oarc-reroll-spawn-command" })
end
-- Check a table to see if there are any players waiting to spawn
-- Check if we are past the delayed tick count
-- Spawn the players and remove them from the table.
---@return nil
function DelayedSpawnOnTick()
if ((game.tick % (30)) == 1) then
if ((storage.delayed_spawns ~= nil) and (#storage.delayed_spawns > 0)) then
-- I think this loop removes from the back of the table to the front??
for i = #storage.delayed_spawns, 1, -1 do
delayed_spawn = storage.delayed_spawns[i] --[[@as OarcDelayedSpawn]]
local surface = game.surfaces[delayed_spawn.surface_name]
if ((delayed_spawn.delayed_tick < game.tick) or surface.is_chunk_generated(delayed_spawn.final_chunk_generated) ) then
log("DelayedSpawnOnTick - Generating spawn for: " .. delayed_spawn.host_name)
GenerateFinalSpawnPieces(delayed_spawn)
storage.unique_spawns[delayed_spawn.surface_name][delayed_spawn.host_name].generated = true
-- For each player waiting to spawn, send them to their new spawn point.
for _,player_name in pairs(delayed_spawn.waiting_players) do
local player = game.players[player_name]
if (player ~= nil) then
SendPlayerToNewSpawn(player_name, delayed_spawn.surface_name, delayed_spawn.primary, delayed_spawn.host_name == player_name)
end
end
table.remove(storage.delayed_spawns, i)
end
end
end
end
end
---Send player to their custom spawn point
---@param surface_name string
---@param player LuaPlayer
---@param first_spawn boolean
---@return nil
function TeleportPlayerToRespawnPoint(surface_name, player, first_spawn)
local spawn = storage.player_respawns[player.name][surface_name]
if (spawn == nil) then
log("ERROR - SendPlayerToSpawn - No spawn point for player: " .. player.name .. " on surface: " .. surface_name .. " first_spawn: " .. tostring(first_spawn))
return
end
if player.driving then
--Ignore the lua warning, the factorio docs use "bool" instead of "boolean" so its broken. TODO: Remove this when they update the docs.
---@diagnostic disable-next-line: param-type-mismatch
player.set_driving(false, true) -- We need to force player out of the cargo-pod before we teleport.
end
SafeTeleport(player, game.surfaces[surface_name], spawn.position)
if first_spawn then
player.permission_group = game.permissions.get_group("Default")
end
end
---Check if a player has a delayed spawn
---@param player_name string
---@return boolean
function PlayerHasDelayedSpawn(player_name)
for _,delayedSpawn in pairs(storage.delayed_spawns) do
if (delayedSpawn.host_name == player_name) then
return true
end
end
return false
end
---Get the list of surfaces that are allowed for primary spawning.
---@return string[]
function GetAllowedSurfaces()
---@type string[]
local surfaceList = {}
for surfaceName, allowed in pairs(storage.oarc_surfaces) do
if allowed.primary then
table.insert(surfaceList, surfaceName)
end
end
return surfaceList
end
--[[
___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
| __|/ _ \ | _ \ / __|| __| / __|| _ \| __|/ __||_ _|| __||_ _|/ __|
| _|| (_) || /| (__ | _| \__ \| _/| _|| (__ | | | _| | || (__
|_| \___/ |_|_\ \___||___| |___/|_| |___|\___||___||_| |___|\___|
--]]
---Create a new player force (sets ceasefire and friendly status for all teams)
---@param force_name string
---@return LuaForce
function CreatePlayerForce(force_name)
local new_force = nil
-- Check if force already exists
if (game.forces[force_name] ~= nil) then
log("Force already exists!")
return CreatePlayerForce(force_name .. "_") -- Append a character to make the force name unique.
-- Create a new force
elseif (#game.forces < MAX_FORCES) then
new_force = game.create_force(force_name)
new_force.share_chart = storage.ocfg.gameplay.enable_shared_team_vision
new_force.friendly_fire = storage.ocfg.gameplay.enable_friendly_fire
-- SetCeaseFireBetweenAllPlayerForces()
-- SetFriendlyBetweenAllPlayerForces()
ConfigurePlayerForceRelationships(storage.ocfg.gameplay.enable_cease_fire,
storage.ocfg.gameplay.enable_friendly_teams)
-- ConfigureEnemyForceRelationshipsForNewPlayerForce(new_force)
else
log("TOO MANY FORCES!!! - CreatePlayerForce()")
return game.forces[storage.ocfg.gameplay.main_force_name]
end
return new_force
end
---Create a new player force and assign the player to it.
---@param player LuaPlayer
---@return LuaForce
function CreatePlayerCustomForce(player)
local newForce = CreatePlayerForce(player.name)
player.force = newForce
if (newForce.name == player.name) then
SendBroadcastMsg({ "oarc-player-started-own-team", player.name })
else
SendErrorMsg(player, { "oarc-player-no-new-teams-sorry" })
end
return newForce
end
--[[
_ _ _ _ _______ _____ ___ _ _ _ _ _ ___ _____ _ _____ ___ ___ _ _ ___
| | | | | |/_\ |_ _\ \ / / _ \ __| /_\ | \| | \| |/ _ \_ _/_\_ _|_ _/ _ \| \| / __|
| |_| |_| / _ \ | | \ V /| _/ _| / _ \| .` | .` | (_) || |/ _ \| | | | (_) | .` \__ \
|____\___/_/ \_\ |_| |_| |_| |___| /_/ \_\_|\_|_|\_|\___/ |_/_/ \_\_| |___\___/|_|\_|___/
These are LUA type annotations for development and editor support.
You can ignore this unless you're making changes to the mod, in which case it might be helpful.
]]
---@enum SpawnTeamChoice
SPAWN_TEAM_CHOICE = {
join_main_team = 1,
join_own_team = 2,
-- join_buddy_team = 3, -- Removed in favor of separate override
}
---Contains the respawn point for a player. Usually this is their home base but it can be changed.
---@alias OarcPlayerSpawn { surface: string, position: MapPosition }
---Table of [OarcSharedSpawn](lua://OarcSharedSpawn) indexed by player name and then by surface name.
---@alias OarcPlayerRespawnsTable table<string, table<string, OarcPlayerSpawn>>
---Class for unique spawn point
---@class OarcUniqueSpawn
---@field surface_name string The surface on which the spawn is located.
---@field position MapPosition The position of the spawn on that surface.
---@field primary boolean Whether this is the primary spawn point for a player, this is the first surface they spawn on. All other spawns are secondary.
---@field moat boolean Whether the spawn has a moat or not.
---@field open_access boolean Whether the spawn is open for other players to join.
---@field host_name string The player name of the host of this spawn.
---@field join_queue string[] List of players waiting to join this spawn.
---@field joiners string[] List of players who have joined this spawn NOT including the host.
---@field buddy_name string? The other buddy player name if this is a buddy spawn.
---@field generated boolean Whether the spawn has finished being generated or not.
---Table of [OarcUniqueSpawnClass](lua://OarcUniqueSpawnClass) indexed first by surface name and then by player name.
---@alias OarcUniqueSpawnsTable table<string, table<string, OarcUniqueSpawn>>
---Contains player ability cooldowns. Right now this only tracks changing the respawn ability.
---@alias OarcPlayerCooldown { setRespawn: number }
---Table of [OarcPlayerCooldown](lua://OarcPlayerCooldown) indexed by player name.
---@alias OarcPlayerCooldownsTable table<string, OarcPlayerCooldown>
---Temporary data used when spawning a player. Player needs to wait while the area is prepared.
---Temporary data used when spawning a player. Player needs to wait while the area is prepared.
---@class OarcDelayedSpawn: OarcUniqueSpawn
---@field delayed_tick number The game tick when the spawn will be ready.
---@field final_chunk_generated ChunkPosition The final chunk position that needs to be generated.
---@field waiting_players string[] List of players waiting to join this spawn once it is done generating.
---Array of [OarcDelayedSpawn](lua://OarcDelayedSpawn).
---@alias OarcDelayedSpawnsTable OarcDelayedSpawn[]
---This contains the spawn choices for a player in the spawn menu.
---Class representing the spawn choices for a player in the spawn menu.
---@class OarcSpawnChoices
---@field surface_name string The surface on which the player wants to spawn.
---@field team SpawnTeamChoice The team choice for the player. Main team or own team.
---@field moat boolean Whether the player wants a moat around their spawn.
---@field buddy string? The buddy player name if the player wants to spawn with a buddy.
---@field distance integer The distance from the center of the map where the player wants to spawn.
---@field host_name string? The host player name if the player wants to join a shared spawn.
---@field buddy_team boolean Whether the player wants to join a buddy's team. This means both players will be on the same team.
---Table of [OarcSpawnChoices](lua://OarcSpawnChoices) indexed by player name.
---@alias OarcSpawnChoicesTable table<string, OarcSpawnChoices>
---Primary means a player can spawn for the first time on this surface, secondary they can land here and also receive a custom spawn area.
---@alias OarcSurfaceSpawnSetting { primary: boolean, secondary: boolean}
---Entry for a nil_character_teleport_queue
---@alias OarcNilCharacterTeleportQueueEntry { surface_name: string, first_spawn: boolean, is_host: boolean }