diff --git a/.gitignore b/.gitignore index 93d43a4..ed7fb70 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -config.lua \ No newline at end of file +releases \ No newline at end of file diff --git a/LICENSE b/LICENSE index cb1cae2..78dc4e8 100755 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 Oarcinae +Copyright (c) 2024 Oarcinae Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8c69aba..34b6ff7 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,26 @@ -# FactorioScenarioMultiplayerSpawn -A custom scenario for allowing separate spawn locations in multiplayer. Designed for Co-op and PvE. +# OARC Multiplayer Spawn +A Factorio mod for allowing separate spawn locations in multiplayer. Designed for Co-op and PvE. + +## WORK IN PROGRESS +This is the currently in development branch for Factorio V2.0. It is not yet complete. If you want a stable version, please use the latest release. ## Read the WIKI The github wiki page will have the most up to date information. If you have questions, go there first, otherwise just ask. ## Versions -Check releases for stable versions. 0.17 and 0.16 have final stable versions that will no longer be maintained. -The latest master is for 1.0.0+ +Check releases for stable versions. +2.0.X WILL BE the currently in work version that will support Factorio V2.0 with and without the Space Age expansion. +1.1.X is the latest stable version for V1.0 that I plan to do minimal maintenance on. +0.17 and 0.16 have final stable versions that will no longer be maintained. ## Status -At this time I have no more planned features and will only be in support and maintenance mode. +Currently working on adding support for V2.0 Factorio. +Will be removing many of the soft-mod features and other feature creep things for the first version. +Waiting on Space Age release so I can start working on support for that. ## Credit -Several other portions of the code (tags, frontier style rocket silo) have also been adapted from other scenario code. - -Credit to 3Ra for help as well: https://github.com/3RaGaming - -Praise be to Mylon +Thanks to https://github.com/vfinn (JustGoFly) and many others for their assistance! +https://github.com/Oarcinae/FactorioScenarioMultiplayerSpawn/graphs/contributors ## Random Notes Feel free to submit bugs/fixes/requests/pulls/forks whatever you want. @@ -24,5 +28,5 @@ Feel free to submit bugs/fixes/requests/pulls/forks whatever you want. I do not plan on supporting PvP, but I will help anyone who wants to make it a configurable option. ## Contact -discord.gg/trnpcen +https://discord.gg/trnpcen oarcinae@gmail.com diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..c93b19f --- /dev/null +++ b/changelog.txt @@ -0,0 +1,22 @@ +--------------------------------------------------------------------------------------------------- +Version: 2.0.0 +Date: 2024-09-25 + Major Features: + - Initial release of a formal mod (non-scenario) version. This is essentially a rewrite of the original scenario in mod form. + - Added support for spawning on different surfaces in preparation for space age support. + - Updated all GUIs to be more in line with style recommendations and to be more user friendly. + - New preview GUI while spawn is being generated and instant transition to the new spawn when ready. + - Exposed most of the mod settings in the custom in game mod GUI. This provides a nicer interface than the native mod settings allows. + - New holding pen surface with restricted permissions for players to spawn into before being moved to their primary spawn. + - Regrowth and world eater features can safely be toggled on/off at any time and supports multiple surfaces. + Bugfixes: + - Fixed an issue where active radars would block spawn areas from being cleaned up if a player left within the removal window. + - Fixed an issue where regrowth would sometimes delete chunks with vehicles, robots or spidertron in them. + - Added several mods as hidden dependencies to avoid fatal errors on startup due to incompatible load order issues. + Optimizations: + - New implementation of shared electricity should stop any possibility of desync with the old method as well as improve performance. (Uses cross-surface power connections.) + - New implementation of shared items should also improve performance. (Uses linked-chest.) + - Some changes to regrowth to improve performance (reducing unnecessary refresh areas). + Info: + - Several of the soft-mod features have been removed (including the coin shop). + - Space age support will not be available until some time after it is released. I will need time to test and implement it. For now, this is provided as a feature for experimentation. You can test "secondary" spawns by enabling that feature in the settings, it is currently disabled by default. \ No newline at end of file diff --git a/compat/factoriomaps.lua b/compat/factoriomaps.lua deleted file mode 100644 index 1a2f46b..0000000 --- a/compat/factoriomaps.lua +++ /dev/null @@ -1,14 +0,0 @@ -Compat = Compat or {} - - -function Compat.handle_factoriomaps() - if remote.interfaces.factoriomaps then - script.on_event(remote.call("factoriomaps", "get_start_capture_event_id"), function() - - print("Starting factoriomaps-oarc integration script") - - remote.call("factoriomaps", "surface_set_default", "oarc") - - end) - end -end \ No newline at end of file diff --git a/control.lua b/control.lua index 830b401..dc1f4be 100644 --- a/control.lua +++ b/control.lua @@ -1,376 +1,219 @@ --- control.lua --- Mar 2019 - --- Oarc's Separated Spawn Scenario --- --- I wanted to create a scenario that allows you to spawn in separate locations --- From there, I ended up adding a bunch of other minor/major features --- --- Credit: --- Tags - Taken from WOGs scenario --- Rocket Silo - Taken from Frontier as an idea --- --- Feel free to re-use anything you want. It would be nice to give me credit --- if you can. - - - --- To keep the scenario more manageable (for myself) I have done the following: --- 1. Keep all event calls in control.lua (here) --- 2. Put all config options in config.lua and provided an example-config.lua file too. --- 3. Put other stuff into their own files where possible. --- 4. Put all other files into lib folder --- 5. Provided an examples folder for example/recommended map gen settings - --- Generic Utility Includes -require("lib/oarc_utils") +-- Sep 2024 +-- ____ _____ _____ +-- / __ \ /\ | __ \ / ____| +-- | | | | / \ | |__) | | +-- | | | |/ /\ \ | _ /| | +-- | |__| / ____ \| | \ \| |____ +-- \____/_/ \_\_| \_\\_____| + +-- Oarc's Separated Spawn MOD V2 +-- I decided to rewrite my old scenario due to the coming changes in Factorio V2.0 and its new Space Age Expansion. + +-- Change Overview: +-- Support the scenario "as a mod" ONLY. Scenario merely provides a way to overwrite settings on_init. +-- Removed a lot of unnecessary feature bloat. +-- Move text to locale files where possible. + +-- Major Features: +-- Core feature allows for a safe, separate spawn area for each player. +-- Players can choose to spawn with friends (buddy spawn) or join other bases. +-- Offline protection from enemy attacks. +-- Chunk cleanup to keep save file size down. +-- Sharing of electricity and items between players. --- Other soft-mod type features. -require("lib/frontier_silo") -require("lib/tag") -require("lib/game_opts") -require("lib/player_list") -require("lib/rocket_launch") -require("lib/admin_commands") +require("lib/oarc_utils") +require("lib/config") +require("lib/config_parser") require("lib/regrowth_map") -require("lib/shared_chests") -require("lib/notepad") -require("lib/map_features") -require("lib/oarc_buy") -require("lib/auto_decon_miners") - --- For Philip. I currently do not use this and need to add proper support for --- commands like this in the future. --- require("lib/rgcommand") --- require("lib/helper_commands") - --- Main Configuration File -require("config") - --- Save all config settings to global table. -require("lib/oarc_global_cfg.lua") - --- Scenario Specific Includes +require("lib/holding_pen") require("lib/separate_spawns") require("lib/separate_spawns_guis") -require("lib/oarc_enemies") require("lib/oarc_gui_tabs") +require("lib/offline_protection") +require("lib/scaled_enemies") +require("lib/sharing") --- compatibility with mods -require("compat/factoriomaps") +-- TODO: Possibly remove this later? +require("lib/oarc_tests") --- Create a new surface so we can modify map settings at the start. -GAME_SURFACE_NAME="oarc" -commands.add_command("trigger-map-cleanup", - "Force immediate removal of all expired chunks (unused chunk removal mod)", - RegrowthForceRemoveChunksCmd) -------------------------------------------------------------------------------- --- ALL EVENT HANLDERS ARE HERE IN ONE PLACE! +-- On Init - Only runs once the first time the game starts -------------------------------------------------------------------------------- - ----------------------------------------- --- On Init - only runs once the first --- time the game starts ----------------------------------------- script.on_init(function(event) - - -- FIRST - InitOarcConfig() - - -- Regrowth (always init so we can enable during play.) + ValidateAndLoadConfig() RegrowthInit() - -- Create new game surface - CreateGameSurface() - - -- MUST be before other stuff, but after surface creation. InitSpawnGlobalsAndForces() + CreateHoldingPenSurface() -- Must be after init spawn globals? - -- Frontier Silo Area Generation - if (global.ocfg.frontier_rocket_silo and not global.ocfg.enable_magic_factories) then - SpawnSilosAndGenerateSiloAreas() - end - - -- Everyone do the shuffle. Helps avoid always starting at the same location. - -- Needs to be done after the silo spawning. - if (global.ocfg.enable_vanilla_spawns) then - global.vanillaSpawns = FYShuffle(global.vanillaSpawns) - log("Vanilla spawns:") - log(serpent.block(global.vanillaSpawns)) - end - - Compat.handle_factoriomaps() - - if (global.ocfg.enable_coin_shop and global.ocfg.enable_chest_sharing) then - SharedChestInitItems() - end - - if (global.ocfg.enable_coin_shop and global.ocfg.enable_magic_factories) then - MagicFactoriesInit() - end - - OarcMapFeatureInitGlobalCounters() - OarcAutoDeconOnInit() - - -- Display starting point text as a display of dominance. - RenderPermanentGroundText(game.surfaces[GAME_SURFACE_NAME], {x=-29,y=-30}, 40, "OARC", {0.9, 0.7, 0.3, 0.8}) -end) - -script.on_load(function() - Compat.handle_factoriomaps() -end) - - ----------------------------------------- --- Rocket launch event --- Used for end game win conditions / unlocking late game stuff ----------------------------------------- -script.on_event(defines.events.on_rocket_launched, function(event) - RocketLaunchEvent(event) -end) - - ----------------------------------------- --- Chunk Generation ----------------------------------------- -script.on_event(defines.events.on_chunk_generated, function(event) - - if (event.surface.name ~= GAME_SURFACE_NAME) then return end - - if global.ocfg.enable_regrowth then - RegrowthChunkGenerate(event) - end - - if global.ocfg.enable_undecorator then - UndecorateOnChunkGenerate(event) - end - - SeparateSpawnsGenerateChunk(event) - - CreateHoldingPen(event.surface, event.area) -end) - - ----------------------------------------- --- Gui Click ----------------------------------------- -script.on_event(defines.events.on_gui_click, function(event) - - -- Don't interfere with other mod related stuff. - if (event.element.get_mod() ~= nil) then return end - - if global.ocfg.enable_tags then - TagGuiClick(event) + -- Useful for debugging and if players choose not to use the provided empty scenario. + if remote.interfaces["freeplay"] then + log("Freeplay interface detected. Disabling various freeplay features now!") + remote.call("freeplay", "set_skip_intro", true) + remote.call("freeplay", "set_disable_crashsite", true) + remote.call("freeplay", "set_created_items", {}) + remote.call("freeplay", "set_respawn_items", {}) end - WelcomeTextGuiClick(event) - SpawnOptsGuiClick(event) - SpawnCtrlGuiClick(event) - SharedSpwnOptsGuiClick(event) - BuddySpawnOptsGuiClick(event) - BuddySpawnWaitMenuClick(event) - BuddySpawnRequestMenuClick(event) - SharedSpawnJoinWaitMenuClick(event) - - ClickOarcGuiButton(event) - - if global.ocfg.enable_coin_shop then - ClickOarcStoreButton(event) + -- If there are any players that already exist, init them now. + for _,player in pairs(game.players) do + SeparateSpawnsInitPlayer(player.index) end - - GameOptionsGuiClick(event) end) -script.on_event(defines.events.on_gui_checked_state_changed, function (event) - SpawnOptsRadioSelect(event) - SpawnCtrlGuiOptionsSelect(event) -end) +-------------------------------------------------------------------------------- +-- On Configuration Changed - Only runs when the mod configuration changes +-------------------------------------------------------------------------------- +-- oarc_new_spawn_created = script.generate_event_name() -script.on_event(defines.events.on_gui_selected_tab_changed, function (event) - TabChangeOarcGui(event) +-- script.on_configuration_changed(function(data) +-- -- Regenerate event ID: +-- end) - if global.ocfg.enable_coin_shop then - TabChangeOarcStore(event) - end +script.on_event(defines.events.on_runtime_mod_setting_changed, function(event) + if (not StringStartsWith(event.setting, "oarc-mod")) then return end + RuntimeModSettingChanged(event) end) ---------------------------------------- -- Player Events ---------------------------------------- -script.on_event(defines.events.on_player_joined_game, function(event) - PlayerJoinedMessages(event) - ServerWriteFile("player_events", game.players[event.player_index].name .. " joined the game." .. "\n") -end) - script.on_event(defines.events.on_player_created, function(event) - local player = game.players[event.player_index] - - -- Move the player to the game surface immediately. - player.teleport({x=0,y=0}, GAME_SURFACE_NAME) - - if global.ocfg.enable_long_reach then - GivePlayerLongReach(player) - end - - SeparateSpawnsPlayerCreated(event.player_index, true) - - InitOarcGuiTabs(player) - - if global.ocfg.enable_coin_shop then - InitOarcStoreGuiTabs(player) - end + SeparateSpawnsInitPlayer(event.player_index) end) script.on_event(defines.events.on_player_respawned, function(event) SeparateSpawnsPlayerRespawned(event) +end) - PlayerRespawnItems(event) +script.on_event(defines.events.on_player_left_game, function(event) + SeparateSpawnsPlayerLeft(event) +end) - if global.ocfg.enable_long_reach then - GivePlayerLongReach(game.players[event.player_index]) - end +script.on_event(defines.events.on_player_changed_surface, function(event) + SeparateSpawnsPlayerChangedSurface(event) end) -script.on_event(defines.events.on_player_left_game, function(event) - ServerWriteFile("player_events", game.players[event.player_index].name .. " left the game." .. "\n") - local player = game.players[event.player_index] - - -- If players leave early, say goodbye. - if (player and (player.online_time < (global.ocfg.minimum_online_time * TICKS_PER_MINUTE))) then - log("Player left early: " .. player.name) - SendBroadcastMsg(player.name .. "'s base was marked for immediate clean up because they left within "..global.ocfg.minimum_online_time.." minutes of joining.") - RemoveOrResetPlayer(player, true, true, true, true) +---------------------------------------- +-- Shared chat, so you don't have to type /s +-- But you do lose your player colors across forces. +---------------------------------------- +script.on_event(defines.events.on_console_chat, function(event) + if (global.ocfg.gameplay.enable_shared_team_chat) then + if (event.player_index ~= nil) then + ShareChatBetweenForces(game.players[event.player_index], event.message) + end end end) --- script.on_event(defines.events.on_player_removed, function(event) - -- Player is already deleted when this is called. --- end) - ---------------------------------------- -- On tick events. Stuff that needs to happen at regular intervals. -- Delayed events, delayed spawns, ... ---------------------------------------- script.on_event(defines.events.on_tick, function(event) - if global.ocfg.enable_regrowth then - RegrowthOnTick() - RegrowthForceRemovalOnTick() - end - DelayedSpawnOnTick() + FadeoutRenderOnTick() - if global.ocfg.enable_chest_sharing then - SharedChestsOnTick() + if global.ocfg.regrowth.enable_regrowth then + RegrowthOnTick() end + RegrowthForceRemovalOnTick() -- Allows for abandoned base cleanup without regrowth enabled. - if (global.ocfg.enable_chest_sharing and global.ocfg.enable_magic_factories) then - MagicFactoriesOnTick() + if global.ocfg.gameplay.modified_enemy_spawning then + RestrictEnemyEvolutionOnTick() end +end) - TimeoutSpeechBubblesOnTick() - FadeoutRenderOnTick() +---------------------------------------- +-- Chunk Generation +---------------------------------------- +script.on_event(defines.events.on_chunk_generated, function(event) + if global.ocfg.regrowth.enable_regrowth then + RegrowthChunkGenerate(event) + end + + CreateHoldingPenChunks(event) + SeparateSpawnsGenerateChunk(event) - if global.ocfg.enable_miner_decon then - OarcAutoDeconOnTick() + if global.ocfg.gameplay.modified_enemy_spawning then + DowngradeWormsDistanceBasedOnChunkGenerate(event) + DowngradeAndReduceEnemiesOnChunkGenerate(event) end end) - +---------------------------------------- +-- Radar Scanning +---------------------------------------- script.on_event(defines.events.on_sector_scanned, function (event) - if global.ocfg.enable_regrowth then + if global.ocfg.regrowth.enable_regrowth then RegrowthSectorScan(event) end end) +---------------------------------------- +-- Surface Generation +---------------------------------------- +-- This is not called when the default surface "nauvis" is created as it will always exist! +script.on_event(defines.events.on_surface_created, function(event) + log("Surface created: " .. game.surfaces[event.surface_index].name) + SeparateSpawnsSurfaceCreated(event) + RegrowthSurfaceCreated(event) +end) + +script.on_event(defines.events.on_pre_surface_deleted, function(event) + log("Surface deleted: " .. game.surfaces[event.surface_index].name) + SeparateSpawnsSurfaceDeleted(event) + RegrowthSurfaceDeleted(event) +end) ---------------------------------------- -- Various on "built" events ---------------------------------------- script.on_event(defines.events.on_built_entity, function(event) - if global.ocfg.enable_autofill then - Autofill(event) + if global.ocfg.regrowth.enable_regrowth then + RegrowthMarkAreaSafeGivenTilePos(event.created_entity.surface.name, event.created_entity.position, 2, false) end - if global.ocfg.enable_regrowth then - if (event.created_entity.surface.name ~= GAME_SURFACE_NAME) then return end - RegrowthMarkAreaSafeGivenTilePos(event.created_entity.position, 2, false) - end - - if global.ocfg.enable_anti_grief then - SetItemBlueprintTimeToLive(event) - end - - if global.ocfg.frontier_rocket_silo then - BuildSiloAttempt(event) - end + -- For tracking spidertrons... + RegrowthOnBuiltEntity(event) + -- if global.ocfg.enable_anti_grief then + -- SetItemBlueprintTimeToLive(event) + -- end end) script.on_event(defines.events.on_robot_built_entity, function (event) - if global.ocfg.enable_regrowth then - if (event.created_entity.surface.name ~= GAME_SURFACE_NAME) then return end - RegrowthMarkAreaSafeGivenTilePos(event.created_entity.position, 2, false) - end - if global.ocfg.frontier_rocket_silo then - BuildSiloAttempt(event) + if global.ocfg.regrowth.enable_regrowth then + RegrowthMarkAreaSafeGivenTilePos(event.created_entity.surface.name, event.created_entity.position, 2, false) end end) script.on_event(defines.events.on_player_built_tile, function (event) - if global.ocfg.enable_regrowth then - if (game.surfaces[event.surface_index].name ~= GAME_SURFACE_NAME) then return end - - for k,v in pairs(event.tiles) do - RegrowthMarkAreaSafeGivenTilePos(v.position, 2, false) + if global.ocfg.regrowth.enable_regrowth then + for _,v in pairs(event.tiles) do + RegrowthMarkAreaSafeGivenTilePos(game.surfaces[event.surface_index].name, v.position, 2, false) end end end) +--If a player gets in or out of a vehicle, mark the area as safe so we don't delete the vehicle by accident. +--Only world eater will clean up these chunks over time if it is enabled. +script.on_event(defines.events.on_player_driving_changed_state, function (event) + if global.ocfg.regrowth.enable_regrowth then + RegrowthMarkAreaSafeGivenTilePos(event.entity.surface.name, event.entity.position, 1, false) + end +end) + ---------------------------------------- -- On script_raised_built. This should help catch mods that -- place items that don't count as player_built and robot_built. -- Specifically FARL. ---------------------------------------- script.on_event(defines.events.script_raised_built, function(event) - if global.ocfg.enable_regrowth then - if (event.entity.surface.name ~= GAME_SURFACE_NAME) then return end - RegrowthMarkAreaSafeGivenTilePos(event.entity.position, 2, false) - end -end) - ----------------------------------------- --- Shared chat, so you don't have to type /s --- But you do lose your player colors across forces. ----------------------------------------- -script.on_event(defines.events.on_console_chat, function(event) - if (event.player_index) then - ServerWriteFile("server_chat", game.players[event.player_index].name .. ": " .. event.message .. "\n") - end - if (global.ocfg.enable_shared_chat) then - if (event.player_index ~= nil) then - ShareChatBetweenForces(game.players[event.player_index], event.message) - end - end -end) - ----------------------------------------- --- On Research Finished --- This is where you can permanently remove researched techs ----------------------------------------- -script.on_event(defines.events.on_research_finished, function(event) - - -- Never allows players to build rocket-silos in "frontier" mode. - if global.ocfg.frontier_rocket_silo and not global.ocfg.frontier_allow_build then - RemoveRecipe(event.research.force, "rocket-silo") - end - - if global.ocfg.lock_goodies_rocket_launch and - (not global.ocore.satellite_sent or not global.ocore.satellite_sent[event.research.force.name]) then - for _,v in ipairs(LOCKED_RECIPES) do - RemoveRecipe(event.research.force, v.r) - end + if global.ocfg.regrowth.enable_regrowth then + RegrowthMarkAreaSafeGivenTilePos(event.entity.surface.name, event.entity.position, 2, false) end end) @@ -379,13 +222,15 @@ end) -- This is where I modify biter spawning based on location and other factors. ---------------------------------------- script.on_event(defines.events.on_entity_spawned, function(event) - if (global.ocfg.modified_enemy_spawning) then - ModifyEnemySpawnsNearPlayerStartingAreas(event) - end + -- if (global.ocfg.gameplay.modified_enemy_spawning) then + -- ModifyEnemySpawnsNearPlayerStartingAreas(event) + -- end end) + script.on_event(defines.events.on_biter_base_built, function(event) - if (global.ocfg.modified_enemy_spawning) then - ModifyEnemySpawnsNearPlayerStartingAreas(event) + if (global.ocfg.gameplay.modified_enemy_spawning) then + -- ModifyEnemySpawnsNearPlayerStartingAreas(event) + ChangeEnemySpawnersToOtherForceOnBuilt(event) end end) @@ -394,58 +239,83 @@ end) -- This is where I remove biter waves on offline players ---------------------------------------- script.on_event(defines.events.on_unit_group_finished_gathering, function(event) - if (global.ocfg.enable_offline_protect) then - OarcModifyEnemyGroup(event.group) + if (global.ocfg.gameplay.enable_offline_protection) then + OarcModifyEnemyGroup(event) end end) ---------------------------------------- --- On Corpse Timed Out --- Save player's stuff so they don't lose it if they can't get to the corpse fast enough. +-- Gui Events ---------------------------------------- -script.on_event(defines.events.on_character_corpse_expired, function(event) - DropGravestoneChestFromCorpse(event.corpse) +script.on_event(defines.events.on_gui_click, function(event) + if not event.element.valid then return end -- Should we ever react to invalid GUI elements? + + SeparateSpawnsGuiClick(event) + + ClickOarcGuiButton(event) + ServerInfoGuiClick(event) + SpawnCtrlGuiClick(event) + SettingsControlsTabGuiClick(event) + SettingsSurfaceControlsTabGuiClick(event) end) +--- Called when LuaGuiElement checked state is changed (related to checkboxes and radio buttons). +script.on_event(defines.events.on_gui_checked_state_changed, function (event) + if not event.element.valid then return end -- Should we ever react to invalid GUI elements? ----------------------------------------- --- On Gui Text Change --- For capturing text entry. ----------------------------------------- -script.on_event(defines.events.on_gui_text_changed, function(event) - NotepadOnGuiTextChange(event) + SeparateSpawnsGuiCheckedStateChanged(event) + + SpawnCtrlGuiOptionsSelect(event) end) +script.on_event(defines.events.on_gui_selected_tab_changed, function (event) + if not event.element.valid then return end -- Should we ever react to invalid GUI elements? + + OarcGuiSelectedTabChanged(event) +end) ----------------------------------------- --- On Gui Closed -- For capturing player escaping custom GUI so we can close it using ESC key. ----------------------------------------- script.on_event(defines.events.on_gui_closed, function(event) - OarcGuiOnGuiClosedEvent(event) - if global.ocfg.enable_coin_shop then - OarcStoreOnGuiClosedEvent(event) - end + OarcGuiClosed(event) end) ----------------------------------------- --- On enemies killed --- For coin generation and stuff ----------------------------------------- -script.on_event(defines.events.on_post_entity_died, function(event) - if (game.surfaces[event.surface_index].name ~= GAME_SURFACE_NAME) then return end - if global.ocfg.enable_coin_shop then - CoinsFromEnemiesOnPostEntityDied(event) - end -end, -{{filter="type", type = "unit"}, {filter="type", type = "unit-spawner"}, {filter="type", type = "turret"}}) +--- For sliders and other value changing elements. +script.on_event(defines.events.on_gui_value_changed, function(event) + if not event.element.valid then return end -- Should we ever react to invalid GUI elements? + + SeparateSpawnsGuiValueChanged(event) + SettingsControlsTabGuiValueChanged(event) +end) + +--- For dropdowns and listboxes. +script.on_event(defines.events.on_gui_selection_state_changed, function(event) + if not event.element.valid then return end -- Should we ever react to invalid GUI elements? + + SeparateSpawnsGuiSelectionStateChanged(event) + SettingsControlsTabGuiSelectionStateChanged(event) +end) + +script.on_event(defines.events.on_gui_text_changed, function(event) + if not event.element.valid then return end -- Should we ever react to invalid GUI elements? + + SettingsControlsTabGuiTextChanged(event) +end) + +script.on_event(defines.events.on_gui_confirmed, function(event) + if not event.element.valid then return end -- Should we ever react to invalid GUI elements? + + SettingsControlsTabGuiTextconfirmed(event) +end) ---------------------------------------- --- Scripted auto decon for miners... +-- Remote Interface ---------------------------------------- -script.on_event(defines.events.on_resource_depleted, function(event) - if global.ocfg.enable_miner_decon then - OarcAutoDeconOnResourceDepleted(event) - end -end) \ No newline at end of file +local oarc_mod_interface = +{ + get_mod_settings = function() + return OCFG + end +} + +remote.add_interface("oarc_mod", oarc_mod_interface) \ No newline at end of file diff --git a/data.lua b/data.lua new file mode 100644 index 0000000..60ab5f5 --- /dev/null +++ b/data.lua @@ -0,0 +1,27 @@ + +-- I created a few custom entities for making it clear that the shared power poles and chests are special. + +local oarc_linked_chest=table.deepcopy(data.raw["container"]["wooden-chest"]) +oarc_linked_chest.type="linked-container" +oarc_linked_chest.name="oarc-linked-chest" +oarc_linked_chest.inventory_type="with_filters_and_bar" +oarc_linked_chest.inventory_size=settings.startup["oarc-mod-linked-chest-size"].value --[[@as integer]] +oarc_linked_chest.picture.layers[1].filename = "__oarc-mod__/graphics/oarc-linked-chest.png" +oarc_linked_chest.picture.layers[1].hr_version.filename = "__oarc-mod__/graphics/hr-oarc-linked-chest.png" + + +local oarc_linked_power=table.deepcopy(data.raw["electric-pole"]["small-electric-pole"]) +oarc_linked_power.name="oarc-linked-power" +oarc_linked_power.pictures.layers[1].filename = "__oarc-mod__/graphics/oarc-electric-pole.png" +oarc_linked_power.pictures.layers[1].hr_version.filename = "__oarc-mod__/graphics/hr-oarc-electric-pole.png" + +data:extend({ + { + type = "sprite", + name = "oarc-mod-sprite-40", + filename = "__oarc-mod__/icon_40x40.png", + width = 40, + height = 40 + }, + oarc_linked_chest, oarc_linked_power +}) diff --git a/devplan.txt b/devplan.txt new file mode 100644 index 0000000..f1e2abe --- /dev/null +++ b/devplan.txt @@ -0,0 +1,141 @@ +ACTIVE ITEMS: + +------------------------------------------------------------------------------------------------------------------------ + +BACKLOG: + +Not specific: + + +Minor: +- If dead when resetting spawn... possibly delay the opening of the welcome GUI or block spawning until character is spawned? +- Expose old enemy scaling as an option? And/or remove unnecessary checks/logs +- Refresh players in admin controls when dropdown is clicked +- Add a setting for forcing primary spawns to only be on default surface maybe? + +Performance: +- User on_nth_tick for any tick % (modulo) operations. +- SeparateSpawnsGenerateChunk should only search for closest spawn once and pass to sub functions! +- Rework world eater to use less find_entities_filtered + +Major: +- Space Age Support (TBD) + + +------------------------------------------------------SPACE AGE--------------------------------------------------------- + +- Map Settings changes? +- Pollution changes (regrowth)? +- Enemy changes? +- Landing pad locations per FORCE limited to 1? +- Surface names for space ships? +- Spawner health tied to evolution? +- Respawn position is surface specific? Each surface needs a separate respawn point? Default respawn behavior? +- Confirm launch into scenario works (V2.0 fix supposedly) -- https://forums.factorio.com/110708 +- Radar quality affects regrowth safe range? +- Update electric pole connections for shared power if things change in V2.0 + +------------------------------------------------------------------------------------------------------------------------ + +Other Ideas, Not Committed: +- Add option to spawn on existing chunks (look for chunks with any entities in them, or use regrowth logic) +- Add option for spawn pen to be on a specified surface (not sure this serves any real purpose) +- Change "search for ungenerated spawn point" to be a "roll" button that the player can re-roll? Maybe it shows on the map in an icon where they might go first? +- Make players join a "holding_pen" force instead of the main force? Might cause issues with chat and vision? +- Separate chest sharing and electricity () +- Consider scrapping any overlapping mod settings from the config. ONLY keep the part that can't easily be done in the mod settings menu... NOT SURE about this. +- Change enable_shared_team_vision to allow players to change this per player (like BNO) +- Change enable_friendly_fire to be per team? +- Allow players to spawn "near" an existing player (by request) +- Allow players to restart at anytime via GUI button (configurable setting by admin) +- Change regrowth to be list of surfaces indexed by surface name? +- Figure out how to reset player inventory on player reset to avoid extra items? (save and load items?) +- Work on space ex support? +- Profile regrowth and try to improve performance ? +- Possibly adjust easy/medium evo factors on new player joined? +- Convert regrowth to a proper mod +- Create shared electricty with a LIMITED output transfer rate using a custom accumulator? +- Cleanup offline protection (Lots of commented out code) +- Custom tips and tricks?? + +--------------------------------------------------------DONE------------------------------------------------------------ + +- First setup the mod settings and lua global settings. +- Test out removing the creation of a custom surface and ensure map settings can be set as needed. Possibly create a separate surface, or just a separate area away from the origin, for starting players. +- Start copying in the core pieces required for the primary spawning mechanic. +- Document config and mod settings using Lua annotation as a custom class +- Document global ocore as a custom class (with subclasses/types as needed) +- Add multiple surfaces to options/settings [As a single boolean.] +- Convert scenario to a mod. +- Add multiple surfaces to Spawn GUI (Drop down selection?) +- Configurable welcome/server messages in mod settings. +- Check and update all functions using surfaces to clearly use either the LuaSurface obj OR a string name. +- Create server settings admin GUI tab +- Figure out how to define custom lua table/data structs to make syntax/linting work? +- Setup multiplayer testing using multiple instances and some batch files. +- Change Near/Far buttons to radio selection w/ text explanation and have a single Spawn button. +- Refactor the spawn menu GUI (don't destroy the menu unless we need to, refresh only the elements we need to, save data to a global using tags?) +- Remove separate buddy spawn menu? +- Remove the shared spawn separate GUI window? +- Redo DisplayBuddySpawnRequestMenu and DisplayBuddySpawnWaitMenu and move their events to the new event handlers +- Refresh the spawn controls GUI when player accepts/rejects +- Redo FindUngeneratedCoordinates to pick a random direction, and use the distance to get a starting point, instead of multiple tries. +- Fix all GUI styling (buttons outside of content) +- Force enable_world_eater to require enable_regrowth +- Add warning for modifying surface settings after gameplay has started +- Tooltips for GUI elements in spawn menu options! +- Check all settings to see which CAN'T be changed during runtime. Possibly move these to startup. +- FIGURE OUT CHUNK NOT IN MAP REGROWTH ISSUE! +- Add validation for default starting surface name +- Make server info headings consistent / Add heading for spawn controls (change button?) +- Confirm regrowth should add new chunks when found (not only on chunk generate?) +- Make disable main team setting work (hide radio button) +- Support run time toggling of enable_shared_team_vision +- Support run time toggling of enable_friendly_fire +- Watch for chunks being generated to be able to move the player to their spawn as soon as it is done with the last chunk. +- If this is their first spawn, give them items. Otherwise don't give new items and don't clear items either? +- Change enable_spawning_on_other_surfaces to a start up setting? +- Test out space expansion +- Compare GUI mod button icons to space ex (looks better??) +- Create surface blacklist setting. +- Enable/Disable and show information about shared spawn based on shared spawn dropdown interactions. +- Ensure updates to regrowth surfaces don't cause errors (update indexes) +- Confirm regrowth does or does not delete chunks with robots in them? +- Show the surface name in shared spawn join GUI +- Show the surface name in the share spawn controls tab +- Add rich text map location in spawn controls tab for current home location +- Move sharedspawns data under unique spawns +- "uniqueSpawns" should have a "primary" flag and be indexed by surface FIRST +- Make respawn locations first be indexed by player, then surface +- Offline protection re-implement! +- Resolve regrowth issue with radars and confirm that when we mark chunks for removal, they can be refreshed still. (trace logic!) +- Move "buddy" info to unique_spawns as well. +- Fix search vector to use more variable vector'ing, always normalize vector, and then ensure the other reliant functions work still. +- Add regrowth settings GUI tab? Not sure how the other settings fit in with a dedicated regrowth tab? Need to be able to enable/disable other surfaces during runtime? +- Test multiple enemy forces to provide a way to scale evolution locally (need to continually set the evo factor back) +- Change main_force_name to a startup setting! New players in spawn area should stay on default "player" force to avoid mod conflicts on player init? +- In spawn controls, add a note if spawn is full (and maybe disable the shared spawn checkbox?) +- Add rich text map location links for new spawns (print to chat) +- Add in square bases again +- TEST resizing spawn areas and moat sizes! +- Create a function to create secondary uniqueSpawns for the same player +- Expose primary AND secondary spawn info in spawn controls GUI tab +- Shared items (proper) +- Shared electricity (proper) (with configurable settings) +- Add a show current respawn location button (same as show spawn location) +- Expose some settings for adjusting easy and medium enemy evolution values +- Add FAQ for enemy modifications +- Add FAQ for item and energy sharing +- FIX GetNextPlayerIndex! +- Lots of localizations! +- List all TO-DOs in code here. +- Remove testing surfaces before release! +- Change default surface selection to be the default surface if multiple are enabled +- Test and make sure scenario settings overrides get written back to mod settings to avoid any out of sync settings. +- Test teleporting to other surfaces +- Test on_player_changed_surface +- Run the profiler +- Pull out general spawn config from surfaces config +- Redo resource placement to be simpler (and make a linear layout for square base) +- Default to selecting SELF in admin controls player dropdown? +- Add refresh chunks around spidertrons based on their vision \ No newline at end of file diff --git a/example-config.lua b/example-config.lua deleted file mode 100644 index 28e3b0c..0000000 --- a/example-config.lua +++ /dev/null @@ -1,473 +0,0 @@ --- example-config.lua (Rename this file to config.lua to use it) --- May 26 2020 (updated on) --- Configuration Options --- --- You should be safe to leave most of the settings here as defaults if you want. --- The only thing you definitely want to change are the welcome messages. - --------------------------------------------------------------------------------- --- Messages --- You will want to change some of these to be your own. --------------------------------------------------------------------------------- - --- This stuff is shown in the welcome GUI and Info panel. Make sure it's valid. -WELCOME_MSG_TITLE = "[INSERT SERVER OWNER MSG HERE test title!]" -WELCOME_MSG = "[INSERT SERVER OWNER MSG HERE test msg!]" -- Printed to player on join as well. -SERVER_MSG = "Rules: Be polite. Ask before changing other players's stuff. Have fun!\n".. -"This server is running a custom scenario that allows individual starting areas on the map." - -SCENARIO_INFO_MSG = "Latest updates in this scenario version:\n".. -"Item & energy sharing system! No attacks on your base while you are offline!\n".. -"This scenario gives you and/or your friends your own starting area.\n".. -"You can be on the main team or your own. All teams are friendly.\n".. -"If you leave in the first 15 minutes, your base and character will be deleted!" - -CONTACT_MSG = "Contact: SteamID:Oarc | oarcinae@gmail.com | Discord:Oarc#8695" -DISCORD_INV = "discord.gg/trnpcen" - ------------------------------------------------------------------------------------------------------------------------- --- Module Enables --- Each of the following things enable special features. These can't be changed once the game starts. ------------------------------------------------------------------------------------------------------------------------- - --- This allows 2 players to spawn next to each other in the wilderness, each with their own starting point. It adds more --- GUI selection options. -ENABLE_BUDDY_SPAWN = true - --- Frontier style rocket silo mode. This means you can't build silos, but some spawn out in the wild for you to use. --- if ENABLE_MAGIC_FACTORIES=false, you will find a few special areas to launch rockets from. --- If ENABLE_MAGIC_FACTORIES=true, you must buy a silo at one of the special chunks. -FRONTIER_ROCKET_SILO_MODE = true - --- Enable Undecorator. Removes decorative items to reduce save file size. -ENABLE_UNDECORATOR = true - --- Enable Tags (Players can add a name-tag to explain what type of role they are doing if they want.) -ENABLE_TAGS = true - --- Enable Long Reach -ENABLE_LONGREACH = true - --- Enable Autofill (My autofill is very simplistic, if you are using a similar mod disable this!) -ENABLE_AUTOFILL = true - --- Enable auto decon of miners (My miner decon is very simplistic, if you are using a similar mod disable this!) -ENABLE_MINER_AUTODECON = true - --- Enable Playerlist -ENABLE_PLAYER_LIST = true -PLAYER_LIST_OFFLINE_PLAYERS = true -- List offline players as well. - --- Enable shared vision between teams (all teams are COOP regardless) -ENABLE_SHARED_TEAM_VISION = true - --- Cleans up unused chunks periodically. Helps keep map size down. -ENABLE_REGROWTH = true --- This removes player bases when they leave shortly after joining. Only works if you have regrowth enabled! -ENABLE_ABANDONED_BASE_REMOVAL = true - --- Enable the research queue by default for all forces. -ENABLE_RESEARCH_QUEUE = true - --- This enables coin drops from enemies and a shop (GUI) to buy stuff from. -ENABLE_COIN_SHOP = false - --- Enable item & energy sharing system. -ENABLE_ITEM_AND_ENERGY_SHARING = false -- REQUIRES ENABLE_COIN_SHOP=true! - --- Enable magic chunks around the map that let you buy powerful factories that smelt/assemble/process very very quickly. -ENABLE_MAGIC_FACTORIES = false -- REQUIRES ENABLE_COIN_SHOP=true! - --- This inhibits enemy attacks on bases where all players are offline. --- Not 100% guaranteed. -ENABLE_OFFLINE_PROTECTION = true - --- This allows you to set the tech price multiplier for the game, but --- have it only affect the main force. We just pad all non-main forces lab prod bonus. --- This has no effect unless the tech multiplier is more than 1! -ENABLE_FORCE_LAB_PROD_BONUS = true - --- Lock various recipes and technologies behind a rocket launch. --- Each team/force must launch their own rocket to unlock this! -LOCK_GOODIES_UNTIL_ROCKET_LAUNCH = true -LOCKED_TECHNOLOGIES = { - {t="atomic-bomb"},{t="power-armor-mk2"},{t="artillery"},{t="spidertron"} -} -LOCKED_RECIPES = { - {r="productivity-module-3"},{r="speed-module-3"} -} - --- Give cheaty items on start. -ENABLE_POWER_ARMOR_QUICK_START = false -ENABLE_MODULAR_ARMOR_QUICK_START = false - ------------------------------------------------------------------------------------------------------------------------- --- MAP CONFIGURATION OPTIONS --- In past versions I had a way to config map settings here to be used for cmd --- line launching, but now you should just be using --map-gen-settings and --- --map-settings option since it works with --start-server-load-scenario --- Read the README.md file for instructions. ------------------------------------------------------------------------------------------------------------------------- - --- This scales resources so that even if you spawn "far away" from the center --- of the map, resources near to your spawn point scale so you aren't --- surrounded by 100M patches or something. This is useful depending on what --- map gen settings you pick. -SCALE_RESOURCES_AROUND_SPAWNS = true - ------------------------------------------------------------------------------------------------------------------------- --- Alien Options ------------------------------------------------------------------------------------------------------------------------- - --- Adjust enemy spawning based on distance to spawns. All it does it make things --- more balanced based on your distance and makes the game a little easier. --- No behemoth worms everywhere just because you spawned far away. --- If you're trying out the vanilla spawning, you might want to disable this. -OARC_MODIFIED_ENEMY_SPAWNING = true - ------------------------------------------------------------------------------------------------------------------------- --- Starting Items ------------------------------------------------------------------------------------------------------------------------- --- Items provided to the player the first time they join -PLAYER_SPAWN_START_ITEMS = { - ["pistol"]=1, - ["firearm-magazine"]=200, - ["iron-plate"]=100, - ["burner-mining-drill"] = 4, - ["stone-furnace"] = 4, - ["coal"] = 50, - ["stone"] = 50, - - ["coin"] = 2500, -- Don't give coins unless you have shared chests enabled. -} - --- Items provided after EVERY respawn (disabled by default) -PLAYER_RESPAWN_START_ITEMS = { - -- ["pistol"]=1, - -- ["firearm-magazine"]=100, -} - ------------------------------------------------------------------------------------------------------------------------- --- Distance Options ------------------------------------------------------------------------------------------------------------------------- - --- This is the radius, in chunks, that a spawn area is from any other generated --- chunks. It ensures the spawn area isn't too near generated/explored/existing --- area. The larger you make this, the further away players will spawn from --- generated map area (even if it is not visible on the map!). -CHECK_SPAWN_UNGENERATED_CHUNKS_RADIUS = 10 - --- Near Distance in chunks --- When a player selects "near" spawn, they will be in or as close to this range as possible. -NEAR_MIN_DIST = 50 -NEAR_MAX_DIST = 100 - --- Far Distance in chunks --- When a player selects "far" spawn, they will be at least this distance away. -FAR_MIN_DIST = 200 -FAR_MAX_DIST = 300 - - - ------------------------------------------------------------------------------------------------------------------------- --- Resource & Spawn Circle Options ------------------------------------------------------------------------------------------------------------------------- - --- This is where you can modify what resources spawn, how much, where, etc. --- Once you have a config you like, it's a good idea to save it for later use --- so you don't lost it if you update the scenario. -OARC_CFG = { - - -- Misc spawn related config. - gen_settings = { - - -- THIS IS WHAT SETS THE SPAWN CIRCLE SIZE! - -- Create a circle of land area for the spawn - -- If you make this much bigger than a few chunks, good luck. - land_area_tiles = CHUNK_SIZE*2, - - -- Allow players to choose to spawn with a moat - moat_choice_enabled = true, - -- If there is a moat, this attempts to connect to land to avoid "turtling" - moat_bridging = true, - - -- If you change the spawn area size, you might have to adjust this as well - moat_size_modifier = 1, - - -- Start resource shape. true = circle, false = square. - resources_circle_shape = true, - - -- Force the land area circle at the spawn to be fully grass - force_grass = true, - - -- Spawn a circle/octagon of trees around the base outline. - tree_circle = true, - tree_octagon = false, - - -- Add a crashed ship like a vanilla game (create_crash_site) - -- Resources go in the ship itself. (5 slots) - -- Wreakage is distributed in small pieces. (I recommend only 1 item type.) - crashed_ship = true, - crashed_ship_resources = { - ["electronic-circuit"] = 200, - ["iron-gear-wheel"] = 100, - ["copper-cable"] = 200, - -- ["spidertron"] = 1, - ["steel-plate"] = 100 - }, - crashed_ship_wreakage = { - ["iron-plate"] = 100 - }, - }, - - -- Safe Spawn Area Options - -- The default settings here are balanced for my recommended map gen settings (close to train world). - safe_area = - { - -- Safe area has no aliens - -- This is the radius in tiles of safe area. - safe_radius = CHUNK_SIZE*6, - - -- Warning area has significantly reduced aliens - -- This is the radius in tiles of warning area. - warn_radius = CHUNK_SIZE*12, - - -- 1 : X (spawners alive : spawners destroyed) in this area - warn_reduction = 20, - - -- Danger area has slightly reduce aliens - -- This is the radius in tiles of danger area. - danger_radius = CHUNK_SIZE*32, - - -- 1 : X (spawners alive : spawners destroyed) in this area - danger_reduction = 5, - }, - - -- Location of water strip (horizontal) - water = { - x_offset = -4, - y_offset = -48, - length = 8 - }, - - -- Handle placement of starting resources - resource_rand_pos_settings = - { - -- Autoplace resources (randomly in circle) - -- This will ignore the fixed x_offset/y_offset values in resource_tiles. - -- Only works for resource_tiles at the moment, not oil patches/water. - enabled = true, - -- Distance from center of spawn that resources are placed. - radius = 45, - -- At what angle (in radians) do resources start. - -- 0 means starts directly east. - -- Resources are placed clockwise from there. - angle_offset = 2.32, -- 2.32 is approx SSW. - -- At what andle do we place the last resource. - -- angle_offset and angle_final determine spacing and placement. - angle_final = 4.46 -- 4.46 is approx NNW. - }, - - -- Resource tiles - -- If you are running with mods like bobs/angels, you'll want to customize this. - resource_tiles = - { - ["iron-ore"] = - { - amount = 1500, - size = 18, - x_offset = -29, - y_offset = 16 - }, - ["copper-ore"] = - { - amount = 1200, - size = 18, - x_offset = -28, - y_offset = -3 - }, - ["stone"] = - { - amount = 1200, - size = 16, - x_offset = -27, - y_offset = -34 - }, - ["coal"] = - { - amount = 1200, - size = 16, - x_offset = -27, - y_offset = -20 - }--, - -- ["uranium-ore"] = - -- { - -- amount = 0, - -- size = 0, - -- x_offset = 17, - -- y_offset = -34 - -- } - - -- ####### Bobs + Angels ####### - -- DISABLE STARTING OIL PATCHES! - -- Coal = coal - -- Saphirite = angels-ore1 - -- Stiratite = angels-ore3 - -- Rubyte = angels-ore5 - -- Bobmonium = angels-ore6 - - -- ########## Bobs Ore ########## - -- Iron = iron-ore - -- Copper = copper-ore - -- Coal = coal - -- Stone = stone - -- Tin = tin-ore - -- Lead (Galena) = lead-ore - - -- See https://github.com/Oarcinae/FactorioScenarioMultiplayerSpawn/issues/11#issuecomment-479724909 - -- for full examples. - }, - - -- Special resource patches like oil - resource_patches = - { - ["crude-oil"] = - { - num_patches = 2, - amount = 900000, - x_offset_start = -3, - y_offset_start = 48, - x_offset_next = 6, - y_offset_next = 0 - } - }, -} - ---------------------------------------- --- Other Forces/Teams Options ---------------------------------------- - --- Separate teams --- This allows you to join your own force/team. Everyone is still COOP/PvE, all --- teams are friendly and cease-fire. -ENABLE_SEPARATE_TEAMS = true - --- Main force is what default players join -MAIN_FORCE = "Main Force" - --- Enable if players can allow others to join their base. --- And specify how many including the host are allowed. -ENABLE_SHARED_SPAWNS = true -MAX_PLAYERS_AT_SHARED_SPAWN = 3 - --- Share local team chat with all teams --- This makes it so you don't have to use /s --- But it also means you can't talk privately with your own team. -ENABLE_SHARED_TEAM_CHAT = true - ---------------------------------------- --- Special Action Cooldowns ---------------------------------------- -RESPAWN_COOLDOWN_IN_MINUTES = 15 - --- Require playes to be online for at least X minutes --- Else their character is removed and their spawn point is freed up for use -MIN_ONLINE_TIME_IN_MINUTES = 15 - --------------------------------------------------------------------------------- --- Frontier Rocket Silo Options --------------------------------------------------------------------------------- - --- Number of silos found in the wild. --- These will spawn in a circle at given distance from the center of the map --- If you set this number too high, you'll have a lot of delay at the start of the game. -SILO_NUM_SPAWNS = 5 - --- How many chunks away from the center of the map should the silo be spawned -SILO_CHUNK_DISTANCE = 200 - --- If this is enabled, you get silos at the positions specified below. --- (The other settings above are ignored in this case.) -SILO_FIXED_POSITION = false - --- If you want to set fixed spawn locations for some silos. -SILO_POSITIONS = {{x = -1000, y = -1000}, - {x = -1000, y = 1000}, - {x = 1000, y = -1000}, - {x = 1000, y = 1000}} - --- Set this to false so that you have to search for the silo's. -ENABLE_SILO_VISION = true - --- Add beacons around the silo (Philip's mod) -ENABLE_SILO_BEACONS = false -ENABLE_SILO_RADAR = false - --- Allow silos to be built by the player, but forces them to build in --- the fixed locations. If this is false, silos are built and assigned --- only to the main force. This can cause a problem for non main forces --- when playing with LOCK_GOODIES_UNTIL_ROCKET_LAUNCH enabled. -ENABLE_SILO_PLAYER_BUILD = true - - --------------------------------------------------------------------------------- --- Long Reach Options --------------------------------------------------------------------------------- -BUILD_DIST_BONUS = 64 -REACH_DIST_BONUS = BUILD_DIST_BONUS -RESOURCE_DIST_BONUS = 2 - --------------------------------------------------------------------------------- --- Autofill Options --------------------------------------------------------------------------------- -AUTOFILL_TURRET_AMMO_QUANTITY = 10 - --------------------------------------------------------------------------------- --- ANTI-Griefing stuff ( I don't personally maintain this as I don't care for it.) --- These things were added from other people's requests/changes and are disabled by default. --------------------------------------------------------------------------------- --- Enable this to disable deconstructing from map view, and setting a time limit --- on ghost placements. -ENABLE_ANTI_GRIEFING = false - --- Makes blueprint ghosts dissapear if they have been placed longer than this --- ONLY has an effect if ENABLE_ANTI_GRIEFING is true! -GHOST_TIME_TO_LIVE = 10 * TICKS_PER_MINUTE - --- I like keeping this off... set to true if you want to shoot your own chests --- and stuff. -ENABLE_FRIENDLY_FIRE = false - - ------------------------------------------------------------------------------------------------------------------------- --- EXPERIMENTAL FEATURES --- The following things are not recommended unless you really know what you are doing and are okay with crashes and --- editing lua code. ------------------------------------------------------------------------------------------------------------------------- - --- This turns on writing chat and certain events to specific files so that I can use that for discord integration. I --- suggest you leave this off unless you know what you are doing. -ENABLE_SERVER_WRITE_FILES = false - --- Enable this to have a vanilla style starting spawn. This changes the experience pretty drastically. If you enable --- this, you will NOT get the option to spawn using the "pre-fab" fixed layout spawns. This is because the spawn types --- just don't balance well with each other. -ENABLE_VANILLA_SPAWNS = false - --- Vanilla spawn point options (only applicable if ENABLE_VANILLA_SPAWNS is enabled.) - --- Num total spawns pre-assigned (minimum number) --- Points are in an even grid layout. -VANILLA_SPAWN_COUNT = 60 - --- Num tiles between each spawn. (I recommend at least 1000) -VANILLA_SPAWN_SPACING = 2000 - --- Silo Islands --- This options is only valid when used with ENABLE_VANILLA_SPAWNS and FRONTIER_ROCKET_SILO_MODE! --- This spreads out rocket silos on every OTHER island/vanilla spawn -SILO_ISLANDS_MODE = false - --- This is part of regrowth, and if both are enabled, any chunks which aren't active and have no entities will --- eventually be deleted over time. DO NOT USE THIS WITH MODS! -ENABLE_WORLD_EATER = false \ No newline at end of file diff --git a/example/example-map-gen-settings.json b/example/example-map-gen-settings.json index d608a5b..f451b0a 100644 --- a/example/example-map-gen-settings.json +++ b/example/example-map-gen-settings.json @@ -10,14 +10,16 @@ "water": 1, "_comment_width+height": "Width and height of map, in tiles; 0 means infinite", - "width": 0, - "height": 0, + "width": 1, + "height": 1, "_starting_area_comment": "Multiplier for 'biter free zone radius'", "starting_area": 1, "peaceful_mode": false, + "default_enable_all_autoplace_controls": true, + "autoplace_controls": { "coal" : {"frequency" : 0.20, "richness" : 10.00, "size" : 0.20}, diff --git a/example/example-map-settings.json b/example/example-map-settings.json index 8961f54..46e17b1 100644 --- a/example/example-map-settings.json +++ b/example/example-map-settings.json @@ -3,7 +3,7 @@ { "recipe_difficulty": 0, "technology_difficulty": 0, - "technology_price_multiplier": 3, + "technology_price_multiplier": 1, "research_queue_setting": "always" }, "pollution": @@ -113,7 +113,8 @@ "min_steps_to_check_path_find_termination": 2000, "start_to_goal_cost_multiplier_to_terminate_path_find": 500.0, "overload_levels": [0, 100, 500], - "overload_multipliers": [2, 3, 4] + "overload_multipliers": [2, 3, 4], + "negative_path_cache_delay_interval": 20 }, "max_failed_behavior_count": 3 } diff --git a/graphics/hr-oarc-electric-pole.png b/graphics/hr-oarc-electric-pole.png new file mode 100644 index 0000000..2a6c07d Binary files /dev/null and b/graphics/hr-oarc-electric-pole.png differ diff --git a/graphics/hr-oarc-linked-chest.png b/graphics/hr-oarc-linked-chest.png new file mode 100644 index 0000000..4bab298 Binary files /dev/null and b/graphics/hr-oarc-linked-chest.png differ diff --git a/graphics/oarc-electric-pole.png b/graphics/oarc-electric-pole.png new file mode 100644 index 0000000..1e40630 Binary files /dev/null and b/graphics/oarc-electric-pole.png differ diff --git a/graphics/oarc-linked-chest.png b/graphics/oarc-linked-chest.png new file mode 100644 index 0000000..c249faf Binary files /dev/null and b/graphics/oarc-linked-chest.png differ diff --git a/icon_40x40.png b/icon_40x40.png new file mode 100644 index 0000000..adc20b6 Binary files /dev/null and b/icon_40x40.png differ diff --git a/info.json b/info.json new file mode 100644 index 0000000..ef9b2c8 --- /dev/null +++ b/info.json @@ -0,0 +1,19 @@ +{ + "name": "oarc-mod", + "version": "2.0.0", + "factorio_version": "1.1", + "title": "Oarc Multiplayer Spawn", + "author": "Oarcinae", + "contact": "Mod Discussion Page, oarcinae@gmail.com, Discord:oarc", + "homepage": "https://github.com/Oarcinae/FactorioScenarioMultiplayerSpawn", + "description": "[[Description is in locale file!]]", + "dependencies": [ + "base >= 1.1.0", + "(?) dangOreus" , + "(?) Milestones", + "(?) space-exploration", + "(?) alien-biomes", + "(?) sonaxaton-research-queue", + "(?) helmod" + ] +} \ No newline at end of file diff --git a/lib/admin_commands.lua b/lib/admin_commands.lua deleted file mode 100644 index 3fd871f..0000000 --- a/lib/admin_commands.lua +++ /dev/null @@ -1,153 +0,0 @@ --- admin_commands.lua --- May 2019 --- --- Yay, admin commands! - -require("lib/oarc_utils") - --- name :: string: Name of the command. --- tick :: uint: Tick the command was used. --- player_index :: uint (optional): The player who used the command. It will be missing if run from the server console. --- parameter :: string (optional): The parameter passed after the command, separated from the command by 1 space. - --- Give yourself or another player, power armor -commands.add_command("give-power-armor-kit", "give a start kit", function(command) - - local player = game.players[command.player_index] - local target = player - - if player ~= nil and player.admin then - if (command.parameter ~= nil) then - if game.players[command.parameter] ~= nil then - target = game.players[command.parameter] - else - target.print("Invalid player target. Double check the player name?") - return - end - end - - GiveQuickStartPowerArmor(target) - player.print("Gave a powerstart kit to " .. target.name) - target.print("You have been given a power armor starting kit!") - end -end) - - -commands.add_command("give-test-kit", "give a start kit", function(command) - - local player = game.players[command.player_index] - local target = player - - if player ~= nil and player.admin then - if (command.parameter ~= nil) then - if game.players[command.parameter] ~= nil then - target = game.players[command.parameter] - else - target.print("Invalid player target. Double check the player name?") - return - end - end - - GiveTestKit(target) - player.print("Gave a test kit to " .. target.name) - target.print("You have been given a test kit!") - end -end) - - -commands.add_command("load-quickbar", "Pre-load quickbar shortcuts", function(command) - - local p = game.players[command.player_index] - - -- 1st Row - p.set_quick_bar_slot(1, "transport-belt"); - p.set_quick_bar_slot(2, "small-electric-pole"); - p.set_quick_bar_slot(3, "inserter"); - p.set_quick_bar_slot(4, "underground-belt"); - p.set_quick_bar_slot(5, "splitter"); - - p.set_quick_bar_slot(6, "coal"); - p.set_quick_bar_slot(7, "repair-pack"); - p.set_quick_bar_slot(8, "gun-turret"); - p.set_quick_bar_slot(9, "stone-wall"); - p.set_quick_bar_slot(10, "radar"); - - -- 2nd Row - p.set_quick_bar_slot(11, "stone-furnace"); - p.set_quick_bar_slot(12, "wooden-chest"); - p.set_quick_bar_slot(13, "steel-chest"); - p.set_quick_bar_slot(14, "assembling-machine-1"); - p.set_quick_bar_slot(15, "assembling-machine-2"); - - p.set_quick_bar_slot(16, nil); - p.set_quick_bar_slot(17, nil); - p.set_quick_bar_slot(18, nil); - p.set_quick_bar_slot(19, nil); - p.set_quick_bar_slot(20, nil); - - -- 3rd Row - p.set_quick_bar_slot(21, "electric-mining-drill"); - p.set_quick_bar_slot(22, "fast-inserter"); - p.set_quick_bar_slot(23, "long-handed-inserter"); - p.set_quick_bar_slot(24, "medium-electric-pole"); - p.set_quick_bar_slot(25, "big-electric-pole"); - - p.set_quick_bar_slot(26, "stack-inserter"); - p.set_quick_bar_slot(27, nil); - p.set_quick_bar_slot(28, nil); - p.set_quick_bar_slot(29, nil); - p.set_quick_bar_slot(30, nil); - - -- 4th Row - p.set_quick_bar_slot(31, "fast-transport-belt"); - p.set_quick_bar_slot(32, "medium-electric-pole"); - p.set_quick_bar_slot(33, "fast-inserter"); - p.set_quick_bar_slot(34, "fast-underground-belt"); - p.set_quick_bar_slot(35, "fast-splitter"); - - p.set_quick_bar_slot(36, "stone-wall"); - p.set_quick_bar_slot(37, "repair-pack"); - p.set_quick_bar_slot(38, "gun-turret"); - p.set_quick_bar_slot(39, "laser-turret"); - p.set_quick_bar_slot(40, "radar"); - - -- 5th Row - p.set_quick_bar_slot(41, "train-stop"); - p.set_quick_bar_slot(42, "rail-signal"); - p.set_quick_bar_slot(43, "rail-chain-signal"); - p.set_quick_bar_slot(44, "rail"); - p.set_quick_bar_slot(45, "big-electric-pole"); - - p.set_quick_bar_slot(46, "locomotive"); - p.set_quick_bar_slot(47, "cargo-wagon"); - p.set_quick_bar_slot(48, "fluid-wagon"); - p.set_quick_bar_slot(49, "pump"); - p.set_quick_bar_slot(50, "storage-tank"); - - -- 6th Row - p.set_quick_bar_slot(51, "oil-refinery"); - p.set_quick_bar_slot(52, "chemical-plant"); - p.set_quick_bar_slot(53, "storage-tank"); - p.set_quick_bar_slot(54, "pump"); - p.set_quick_bar_slot(55, nil); - - p.set_quick_bar_slot(56, "pipe"); - p.set_quick_bar_slot(57, "pipe-to-ground"); - p.set_quick_bar_slot(58, "assembling-machine-2"); - p.set_quick_bar_slot(59, "pump"); - p.set_quick_bar_slot(60, nil); - - -- 7th Row - p.set_quick_bar_slot(61, "roboport"); - p.set_quick_bar_slot(62, "logistic-chest-storage"); - p.set_quick_bar_slot(63, "logistic-chest-passive-provider"); - p.set_quick_bar_slot(64, "logistic-chest-requester"); - p.set_quick_bar_slot(65, "logistic-chest-buffer"); - - p.set_quick_bar_slot(66, "logistic-chest-active-provider"); - p.set_quick_bar_slot(67, "logistic-robot"); - p.set_quick_bar_slot(68, "construction-robot"); - p.set_quick_bar_slot(69, nil); - p.set_quick_bar_slot(70, nil); - -end) \ No newline at end of file diff --git a/lib/auto_decon_miners.lua b/lib/auto_decon_miners.lua deleted file mode 100644 index 1d1f923..0000000 --- a/lib/auto_decon_miners.lua +++ /dev/null @@ -1,43 +0,0 @@ --- auto_decon_miners.lua --- May 2020 --- My shitty softmod version which is buggy - -function OarcAutoDeconOnInit(event) - if (not global.oarc_decon_miners) then - global.oarc_decon_miners = {} - end -end - -function OarcAutoDeconOnTick() - if (global.oarc_decon_miners and (#global.oarc_decon_miners > 0)) then - for i,miner in pairs(global.oarc_decon_miners) do - if ((not miner) or (not miner.valid)) then - table.remove(global.oarc_decon_miners, i) - - else - if (#miner.surface.find_entities_filtered{area = {{miner.position.x-3, miner.position.y-3}, - {miner.position.x+3, miner.position.y+3}}, - type = "resource", limit = 1} == 0) then - miner.order_deconstruction(miner.force) - end - table.remove(global.oarc_decon_miners, i) - end - end - end -end - -function OarcAutoDeconOnResourceDepleted(event) - if (not global.oarc_decon_miners) then - global.oarc_decon_miners = {} - end - if (event.entity and event.entity.position and event.entity.surface) then - - local nearby_miners = event.entity.surface.find_entities_filtered{area = {{event.entity.position.x-1, event.entity.position.y-1}, - {event.entity.position.x+1, event.entity.position.y+1}}, - name = {"burner-mining-drill", "electric-mining-drill"}} - - for i,v in pairs(nearby_miners) do - table.insert(global.oarc_decon_miners, v) - end - end -end diff --git a/lib/config.lua b/lib/config.lua new file mode 100644 index 0000000..660df11 --- /dev/null +++ b/lib/config.lua @@ -0,0 +1,519 @@ +--[[ + __ __ ___ ___ __ ___ ___ _ _ ___ _ _ ___ + | \/ |/ _ \| \ \ \ / /_\ | _ \ \| |_ _| \| |/ __| + | |\/| | (_) | |) | \ \/\/ / _ \| / .` || || .` | (_ | + |_| |_|\___/|___/ \_/\_/_/ \_\_|_\_|\_|___|_|\_|\___| + + DO NOT EDIT THIS FILE! + DO NOT EDIT THIS FILE! + DO NOT EDIT THIS FILE! + + If you want to customize settings at init, edit the control.lua file in the scenarios/OARC folder and load that scenario! + +]] + +-- More settings are available here than are provided in the mod settings menu. +-- Additionally, many settings are exposed in the game itself and can be changed in game. +-- For convenience you, you can edit the control.lua file in the scenarios/OARC folder to override settings as well. +-- That method is useful for headless server hosting where it's hard to configure mod settings. + +---@alias SpawnShapeChoice "circle" | "octagon" | "square" +SPAWN_SHAPE_CHOICE_CIRCLE = "circle" +SPAWN_SHAPE_CHOICE_OCTAGON = "octagon" +SPAWN_SHAPE_CHOICE_SQUARE = "square" + +---@alias SpawnResourcesShapeChoice "circle" | "square" +RESOURCES_SHAPE_CHOICE_CIRCLE = "circle" +RESOURCES_SHAPE_CHOICE_SQUARE = "square" + +---@type OarcConfigStartingItems +NAUVIS_STARTER_ITEMS = +{ + player_start_items = { + ["pistol"]=1, + ["firearm-magazine"]=200, + ["iron-plate"]=100, + ["burner-mining-drill"] = 4, + ["stone-furnace"] = 4, + ["coal"] = 50, + ["stone"] = 50, + }, + player_respawn_items = { + -- ["pistol"]=1, + -- ["firearm-magazine"]=100, + }, + + crashed_ship = true, + crashed_ship_resources = { + ["electronic-circuit"] = 200, + ["iron-gear-wheel"] = 100, + ["copper-cable"] = 200, + ["steel-plate"] = 100 + }, + crashed_ship_wreakage = { + ["iron-plate"] = 100 -- I don't recommend more than 1 item type here! + }, +} + +---@type OarcConfigSpawn +NAUVIS_SPAWN_CONFIG = +{ + -- Safe Spawn Area Options + -- The default settings here are balanced for my recommended map gen settings (close to train world). + safe_area = + { + -- Safe area has no aliens + -- This is the radius in tiles of safe area. + safe_radius = CHUNK_SIZE*6, + + -- Warning area has significantly reduced aliens + -- This is the radius in tiles of warning area. + warn_radius = CHUNK_SIZE*12, + + -- 1 : X (spawners alive : spawners destroyed) in this area + warn_reduction = 20, + + -- Danger area has slightly reduced aliens + -- This is the radius in tiles of danger area. + danger_radius = CHUNK_SIZE*32, + + -- 1 : X (spawners alive : spawners destroyed) in this area + danger_reduction = 5, + }, + + -- Location of water strip within the spawn area (2 horizontal rows) + -- The offset is from the TOP (NORTH) of the spawn area. + water = { + x_offset = -4, + y_offset = 10, + length = 8, + }, + + -- Location of shared power pole within the spawn area (if enabled) + -- The offset is from the RIGHT (WEST) of the spawn area. + shared_power_pole_position = { + x_offset=-10, + y_offset=0 + }, + + -- Location of shared chest within the spawn area (if enabled) + -- The offset is from the RIGHT (WEST) of the spawn area. + shared_chest_position = { + x_offset=-10, + y_offset=1 + }, + + -- Solid resource tiles + -- If you are running with mods that add or change resources, you'll want to customize this. + -- Offsets only are applicable if auto placement is disabled. Offsets are from CENTER of spawn area. + solid_resources = { + ["iron-ore"] = { + amount = 1500, + size = 21, + x_offset = -29, + y_offset = 16 + }, + ["copper-ore"] = { + amount = 1200, + size = 21, + x_offset = -28, + y_offset = -3 + }, + ["stone"] = { + amount = 1200, + size = 21, + x_offset = -27, + y_offset = -34 + }, + ["coal"] = { + amount = 1200, + size = 21, + x_offset = -27, + y_offset = -20 + } + }, + + -- Fluid resource patches like oil + -- If you are running with mods that add or change resources, you'll want to customize this. + -- The offset is from the BOTTOM (SOUTH) of the spawn area. + fluid_resources = + { + ["crude-oil"] = + { + num_patches = 2, + amount = 900000, + -- Starting position offset (relative to bottom/south of spawn area) + x_offset_start = -3, + y_offset_start = -10, + -- Additional position offsets for each new oil patch (relative to previous oil patch) + x_offset_next = 6, + y_offset_next = 0 + } + }, +} + +---@type OarcConfigSurface +NAUVIS_SURFACE_CONFIG = +{ + starting_items = NAUVIS_STARTER_ITEMS, + spawn_config = NAUVIS_SPAWN_CONFIG +} + +---@type OarcConfig +OCFG = { + + -- Server Info - This stuff is shown in the welcome GUI and Info panel. + ---@type OarcConfigServerInfo + server_info = { + welcome_msg_title = "Insert Server Title Here!", + welcome_msg = "Insert Server Welcome Message Here!", + discord_invite = "Insert Discord Invite Here!" + }, + + -- General gameplay related settings that I didn't want to expose in the mod settings since these should + -- basically always be enabled unless you're making serious changes. + gameplay = { + + -- Default setting for enabling spawning on other surfaces other than the default_surface. + -- This is a STARTUP setting, so it can't be changed in game!! + -- This is a STARTUP setting, so it can't be changed in game!! + default_allow_spawning_on_other_surfaces = true, + + -- The name of the main force. + -- This is a STARTUP setting, so it can't be changed in game!! + -- This is a STARTUP setting, so it can't be changed in game!! + main_force_name = "Main Force", + + -- At least one of these must be enabled! (enable_main_team and enable_separate_teams) + -- Otherwise we default to enable_main_team = true + -- Allow all players to join a primary force(team). + enable_main_team = true, + + -- Allow players to create their own force(team). + enable_separate_teams = true, + + -- Allow players to choose to spawn with a moat + allow_moats_around_spawns = true, + + -- If there is a moat, this makes a small path to land to avoid "turtling", but if the spawn + -- is in the middle of water, it won't do anything. + enable_moat_bridging = false, + + -- This is the radius, in chunks, that a spawn area is from any other generated + -- chunks. It ensures the spawn area isn't too near generated/explored/existing + -- area. The larger you make this, the further away players will spawn from + -- generated map area (even if it is not visible on the map!). + minimum_distance_to_existing_chunks = 10, + + -- The range in which a player can select how close to the center of the map they want to spawn. + near_spawn_distance = 100, + far_spawn_distance = 500, + + -- This allows 2 players to spawn next to each other, each with their own starting area. + enable_buddy_spawn = true, + + -- This inhibits enemy attacks on bases where all players are offline. Not 100% guaranteed! + enable_offline_protection = true, + + -- Enable shared vision between teams (all teams are COOP regardless) + enable_shared_team_vision = true, + + -- Share local team chat with all teams + -- This makes it so you don't have to use /s + -- But it also means you can't talk privately with your own team. + enable_shared_team_chat = true, + + -- Enable if players can allow others to join their base. + -- And specify how many including the host are allowed. + enable_shared_spawns = true, + number_of_players_per_shared_spawn = 3, + + -- I like keeping this off... set to true if you want to shoot your own chests and stuff. + enable_friendly_fire = false, + + -- The default starting surface. + default_surface = "nauvis", + + -- Enable secondary spawns for players. + -- This automatically creates a new spawn point when they first move to a separate spawns enabled surface. + enable_secondary_spawns = false, + + -- This scales resources so that even if you spawn "far away" from the center + -- of the map, resources near to your spawn point scale so you aren't + -- surrounded by 100M patches or something. This is useful depending on what + -- map gen settings you pick. + scale_resources_around_spawns = true, + + -- Adjust enemy spawning based on distance to spawns. All it does it make things + -- more balanced based on your distance and makes the game a little easier. + -- No behemoth worms everywhere just because you spawned far away. + -- If you're trying out the vanilla spawning, you might want to disable this. + modified_enemy_spawning = true, + + -- Enemy evolution factor for the easy force (inside warning area). + modified_enemy_easy_evo = 0.0, + + -- Enemy evolution factor for the medium force (inside danger area). + modified_enemy_medium_evo = 0.3, + + -- Require playes to be online for at least X minutes + -- Else their character is removed and their spawn point is freed up for use + minimum_online_time = 15, + + -- Respawn cooldown in minutes. + respawn_cooldown_min = 5, + + -- Enable shared power between bases. + -- Creates a special power pole for cross surface connections. + enable_shared_power = false, + + -- Enables a single shared chest using the native linked-chest entity in factorio. + enable_shared_chest = false, + }, + + -- This is a separate feature that is part of the mod that helps keep the map size down. Not required but useful. + regrowth = { + -- Cleans up unused chunks periodically. Helps keep map size down. + -- See description in regrowth_map.lua for more details. + enable_regrowth = false, + + -- This is part of regrowth, and if both are enabled, any chunks which aren't active and have no entities + -- will eventually be deleted over time. If this is disabled, any chunk with a player built entity will be + -- marked permanently safe even if it is removed at a later time. + -- DO NOT USE THIS WITH MODS! (unless you know what you're doing?) + enable_world_eater = false, + + -- This removes player bases when they leave shortly after joining. + enable_abandoned_base_cleanup = true, + + -- This is the interval in minutes that the regrowth cleanup will run. + cleanup_interval = 60, + }, + + -- General spawn settings (size, shape, etc.) + spawn_general = { + + -- Create a circle of land area for the spawn + -- If you make this much bigger than a few chunks, good luck! + -- (It takes a long time to generate new chunks!) + spawn_radius_tiles = CHUNK_SIZE*2, + + -- Width of the moat around the spawn area. + -- If you change the spawn area size, you might have to adjust this as well. + moat_width_tiles = 8, + + -- Width of the tree ring around the spawn area. + -- If you change the spawn area size, you might have to adjust this as well. + tree_width_tiles = 5, + + -- Starting resources deposits shape. + resources_shape = RESOURCES_SHAPE_CHOICE_CIRCLE, + + -- Force the land area circle at the spawn to be fully grass, otherwise it defaults to the existing terrain + -- or uses landfill. + force_grass = false, + + -- Spawn a circle/octagon/square of trees around this base outline. + shape = SPAWN_SHAPE_CHOICE_CIRCLE, + }, + + -- Handle placement of starting resources within the spawn area. + resource_placement = + { + -- Autoplace resources (randomly in circle) + -- This will ignore the fixed x_offset/y_offset values in solid_resources. + -- Only works for solid_resources at the moment, not oil patches/water. + enabled = true, + + -- Distance in tiles from the edge of spawn that resources are placed. Only applicable for circular spawns. + distance_to_edge = 20, + + -- At what angle (in radians) do resources start. + -- 0 means starts directly east. + -- Resources are placed clockwise from there. + angle_offset = 2.32, -- 2.32 is approx SSW. + + -- At what andle do we place the last resource. + -- angle_offset and angle_final determine spacing and placement. + angle_final = 4.46, -- 4.46 is approx NNW. + + -- Vertical offset in tiles for the deposit resource placement. Starting from top-left corner. + -- Only applicable for square spawns. + vertical_offset = 20, + + -- Horizontal offset in tiles for the deposit resource placement. Starting from top-left corner. + -- Only applicable for square spawns. + horizontal_offset = 20, + + -- Spacing between resource deposits in tiles. + -- Only applicable for square spawns. + linear_spacing = 6, + + -- Size multiplier for the starting resource deposits. + size_multiplier = 1.0, + + -- Amount multiplier for the starting resource deposits. + amount_multiplier = 1.0, + + }, + + -- Spawn configuration specific to each surface, including starting & respawn items. + ---@type table + surfaces_config = + { + ["nauvis"] = { + starting_items = NAUVIS_STARTER_ITEMS, + spawn_config = NAUVIS_SPAWN_CONFIG + }, + ["vulcanus"] = { + starting_items = NAUVIS_STARTER_ITEMS, + spawn_config = NAUVIS_SPAWN_CONFIG + }, + ["fulgora"] = { + starting_items = NAUVIS_STARTER_ITEMS, + spawn_config = NAUVIS_SPAWN_CONFIG + }, + ["gleba"] = { + starting_items = NAUVIS_STARTER_ITEMS, + spawn_config = NAUVIS_SPAWN_CONFIG + }, + ["aquilo"] = { + starting_items = NAUVIS_STARTER_ITEMS, + spawn_config = NAUVIS_SPAWN_CONFIG + } + }, + + -- Surfaces blacklist (Ignore these surfaces completely for spawning and regrowth!) + ---@type table + surfaces_blacklist = { + HOLDING_PEN_SURFACE_NAME, + }, + + -- Surfaces blacklist that match THE START of these strings + -- (Ignore these surfaces completely for spawning and regrowth!) + ---@type table + surfaces_blacklist_match = { + -- Factorissimo Mod Surfaces + "factory-power", + "factory-floor", + }, +} + + + + + +--[[ + + _ _ _ _ _______ _____ ___ _ _ _ _ _ ___ _____ _ _____ ___ ___ _ _ ___ + | | | | | |/_\ |_ _\ \ / / _ \ __| /_\ | \| | \| |/ _ \_ _/_\_ _|_ _/ _ \| \| / __| + | |_| |_| / _ \ | | \ 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. +]] + +---@class OarcConfig +---@field server_info OarcConfigServerInfo Personalized server info for the welcome GUI and Info panel. +---@field gameplay OarcConfigGameplaySettings Various mod gameplay settings +---@field regrowth OarcConfigRegrowth Regrowth specific settings (keeps map size down) +---@field spawn_general OarcConfigSpawnGeneral General spawn settings (size, shape, etc.) +---@field resource_placement OarcConfigSpawnResourcePlacementSettings Resource placement settings +---@field surfaces_config table Spawn configuration (starting items and spawn area config) for each surface. +---@field surfaces_blacklist table List of surfaces to ignore automatically. +---@field surfaces_blacklist_match table List of surfaces to ignore automatically if the start of the string matches the surface name. + +---@class OarcConfigServerInfo +---@field welcome_msg_title string Title of welcome GUI window. +---@field welcome_msg string Main welcome message. (Should provide mod info.) +---@field discord_invite string Discord invite for easy copy paste. + +---@class OarcConfigGameplaySettings +---@field default_allow_spawning_on_other_surfaces boolean Default setting for enabling spawning on other surfaces other than the default_surface. This is a STARTUP setting, so it can't be changed in game. +---@field main_force_name string The name of the main force. This is a STARTUP setting, so it can't be changed in game. +---@field enable_main_team boolean Allows all players to join a primary force(team). +---@field enable_separate_teams boolean Allows players to create their own force(team). +---@field allow_moats_around_spawns boolean Allow players to choose to spawn with a moat +---@field enable_moat_bridging boolean If there is a moat, this makes a small path to land to avoid "turtling", but if the spawn is in the middle of water, it won't do anything. +---@field minimum_distance_to_existing_chunks number The radius, in chunks, that a spawn area is from any other generated chunks. It ensures the spawn area isn't too near generated/explored/existing area. +---@field near_spawn_distance number The closest a player can spawn to the origin. (Not exact, but close). +---@field far_spawn_distance number The furthest a player can spawn from the origin. (Not exact, but close). +---@field enable_buddy_spawn boolean Allow 2 players to spawn next to each other, each with their own starting area. +---@field enable_offline_protection boolean Inhibits enemy attacks on bases where all players are offline. Not 100% guaranteed! +---@field enable_shared_team_vision boolean Enable shared vision between teams (all teams are COOP regardless) +---@field enable_shared_team_chat boolean Share local team chat with all teams +---@field enable_shared_spawns boolean Enable if players can allow others to join their spawn. +---@field number_of_players_per_shared_spawn number Number of players allowed to join a shared spawn. +---@field enable_friendly_fire boolean Set to true if you want to shoot your own chests and stuff. +---@field default_surface string The starting surface of the main force. +---@field enable_secondary_spawns boolean Enable secondary spawns for players. This automatically creates a new spawn point when they first move to a separate spawns enabled surface. +---@field scale_resources_around_spawns boolean Scales resources so that even if you spawn "far away" from the center of the map, resources near to your spawn point scale so you aren't surrounded by 100M patches or something. This is useful depending on what map gen settings you pick. +---@field modified_enemy_spawning boolean Adjust enemy spawning based on distance to spawns. All it does it make things more balanced based on your distance and makes the game a little easier. No behemoth worms everywhere just because you spawned far away. +---@field modified_enemy_easy_evo number Enemy evolution factor for the easy force (inside warning area). +---@field modified_enemy_medium_evo number Enemy evolution factor for the medium force (inside danger area). +---@field minimum_online_time number Require playes to be online for at least X minutes Else their character is removed and their spawn point is freed up for use +---@field respawn_cooldown_min number Respawn cooldown in minutes. +---@field enable_shared_power boolean Enable shared power between bases. Creates a special power pole for cross surface connections. +---@field enable_shared_chest boolean Enables a single shared chest using the native linked-chest entity in factorio. + +---@class OarcConfigRegrowth +---@field enable_regrowth boolean Cleans up unused chunks periodically. Helps keep map size down. +---@field enable_world_eater boolean Checks inactive chunks to see if they are empty of entities and deletes them periodically. +---@field enable_abandoned_base_cleanup boolean Removes player bases when they leave shortly after joining. +---@field cleanup_interval number This is the interval in minutes that the regrowth cleanup will run. + +---@class OarcConfigSurface +---@field starting_items OarcConfigStartingItems Starting items for players on this surface (including crashed ship items) +---@field spawn_config OarcConfigSpawn Spawn area config for this surface + +---@class OarcConfigStartingItems +---@field crashed_ship boolean Add a crashed ship like a vanilla game (create_crash_site) Resources go in the ship itself. (5 slots max!) Wreakage is distributed in small pieces. (I recommend only 1 item type.) +---@field crashed_ship_resources table Items to be placed in the crashed ship. +---@field crashed_ship_wreakage table Items to be placed in the crashed ship. (Recommend only 1 item type!) +---@field player_start_items table Items provided to the player the first time they join +---@field player_respawn_items table Items provided after EVERY respawn (disabled by default) + +---@class OarcConfigSpawn +---@field safe_area OarcConfigSpawnSafeArea How safe is the spawn area? +---@field water OarcConfigSpawnWater Water strip settings +---@field shared_power_pole_position OarcOffsetPosition Location of shared power pole relative to spawn center (if enabled) +---@field shared_chest_position OarcOffsetPosition Location of shared chest relative to spawn center (if enabled) +---@field solid_resources table Spawn area config for solid resource tiles +---@field fluid_resources table Spawn area config for fluid resource patches (like oil) + +---@class OarcConfigSpawnGeneral +---@field spawn_radius_tiles number THIS IS WHAT SETS THE SPAWN CIRCLE SIZE! Create a circle of land area for the spawn If you make this much bigger than a few chunks, good luck. +---@field moat_width_tiles number Width of the moat around the spawn area. If you change the spawn area size, you might have to adjust this as well. +---@field tree_width_tiles number Width of the tree ring around the spawn area. If you change the spawn area size, you might have to adjust this as well. +---@field resources_shape SpawnResourcesShapeChoice The starting resources deposits shape. +---@field force_grass boolean Force the land area circle at the spawn to be fully grass, otherwise it defaults to the existing terrain. +---@field shape SpawnShapeChoice Spawn a circle/octagon/square of trees around this base outline. + +---@class OarcConfigSpawnSafeArea +---@field safe_radius number Safe area has no aliens This is the radius in tiles of safe area. +---@field warn_radius number Warning area has significantly reduced aliens This is the radius in tiles of warning area. +---@field warn_reduction number 1 : X (spawners alive : spawners destroyed) in this area +---@field danger_radius number Danger area has slightly reduce aliens This is the radius in tiles of danger area. +---@field danger_reduction number 1 : X (spawners alive : spawners destroyed) in this area + +---@class OarcConfigSpawnWater +---@field x_offset number Location of water strip within the spawn area (horizontal) +---@field y_offset number Location of water strip within the spawn area (vertical) +---@field length number Length of water strip within the spawn area + +---@alias OarcOffsetPosition { x_offset: number, y_offset: number } An offset position intended to be relative to the spawn center. + +---@class OarcConfigSpawnResourcePlacementSettings +---@field enabled boolean Autoplace resources. This will ignore the fixed x_offset/y_offset values in solid_resources. Only works for solid_resources at the moment, not oil patches/water. +---@field distance_to_edge number Distance in tiles from the edge of spawn that resources are placed. Only applicable for circular spawns. +---@field angle_offset number At what angle (in radians) do resources start. 0 means starts directly east. Resources are placed clockwise from there. Only applicable for circular spawns. +---@field angle_final number At what andle do we place the last resource. angle_offset and angle_final determine spacing and placement. Only applicable for circular spawns. +---@field vertical_offset number Vertical offset in tiles for the deposit resource placement. Only applicable for square spawns. +---@field horizontal_offset number Horizontal offset in tiles for the deposit resource placement. Only applicable for square spawns. +---@field linear_spacing number Spacing between resource deposits in tiles. Only applicable for square spawns. +---@field size_multiplier number Size multiplier for the starting resource deposits. +---@field amount_multiplier number Amount multiplier for the starting resource deposits. + +---@alias OarcConfigSolidResource { amount: integer, size: integer, x_offset: integer, y_offset: integer } Amount and placement of solid resource tiles in the spawn area. +---@alias OarcConfigFluidResource { num_patches: integer, amount: integer, x_offset_start: integer, y_offset_start: integer, x_offset_next: integer, y_offset_next: integer } Amount and placement of fluid resource patches in the spawn area. \ No newline at end of file diff --git a/lib/config_parser.lua b/lib/config_parser.lua new file mode 100644 index 0000000..01ec7f5 --- /dev/null +++ b/lib/config_parser.lua @@ -0,0 +1,370 @@ +-- This file is used to validate the config.lua file and handle any mod settings conflicts. +-- DON'T JUDGE ME! I wanted to try and make a nice in game setting GUI since the native mod settings GUI is so limited. + +---Provides a way to look up the config settings key from the mod settings key. +---@alias OarcSettingsLookup { mod_key: string, ocfg_keys: table, type: string, text: LocalisedString? } + +---@type table +OCFG_KEYS = +{ + ["server_info_HEADER"] = {mod_key = "" , ocfg_keys = {""}, type = "header", text = {"oarc-settings-section-header-server-info"}}, + ["server_info.welcome_msg_title"] = {mod_key = "oarc-mod-welcome-msg-title" , ocfg_keys = {"server_info", "welcome_msg_title"}, type = "string"}, + ["server_info.welcome_msg"] = {mod_key = "oarc-mod-welcome-msg" , ocfg_keys = {"server_info", "welcome_msg"}, type = "string"}, + ["server_info.discord_invite"] = {mod_key = "oarc-mod-discord-invite" , ocfg_keys = {"server_info", "discord_invite"}, type = "string"}, + + ["gameplay_HEADER"] = {mod_key = "" , ocfg_keys = {""}, type = "header", text = {"oarc-settings-section-header-gameplay"}}, + ["gameplay_spawn_choices_SUBHEADER"] = {mod_key = "" , ocfg_keys = {""}, type = "subheader", text = {"oarc-settings-section-subheader-spawn-choices"}}, + ["gameplay.enable_main_team"] = {mod_key = "oarc-mod-enable-main-team" , ocfg_keys = {"gameplay", "enable_main_team"}, type = "boolean"}, + ["gameplay.enable_separate_teams"] = {mod_key = "oarc-mod-enable-separate-teams" , ocfg_keys = {"gameplay", "enable_separate_teams"}, type = "boolean"}, + -- STARTUP ["gameplay.enable_spawning_on_other_surfaces"] = {mod_key = "oarc-mod-default-allow-spawning-on-other-surfaces" , ocfg_keys = {"gameplay", "enable_spawning_on_other_surfaces"}, type = "boolean"}, + ["gameplay.allow_moats_around_spawns"] = {mod_key = "oarc-mod-allow-moats-around-spawns" , ocfg_keys = {"gameplay", "allow_moats_around_spawns"}, type = "boolean"}, + ["gameplay.enable_moat_bridging"] = {mod_key = "oarc-mod-enable-moat-bridging" , ocfg_keys = {"gameplay", "enable_moat_bridging"}, type = "boolean"}, + ["gameplay.minimum_distance_to_existing_chunks"] = {mod_key = "oarc-mod-minimum-distance-to-existing-chunks" , ocfg_keys = {"gameplay", "minimum_distance_to_existing_chunks"}, type = "integer"}, + ["gameplay.near_spawn_distance"] = {mod_key = "oarc-mod-near-spawn-distance" , ocfg_keys = {"gameplay", "near_spawn_distance"}, type = "integer"}, + ["gameplay.far_spawn_distance"] = {mod_key = "oarc-mod-far-spawn-distance" , ocfg_keys = {"gameplay", "far_spawn_distance"}, type = "integer"}, + ["gameplay.enable_buddy_spawn"] = {mod_key = "oarc-mod-enable-buddy-spawn" , ocfg_keys = {"gameplay", "enable_buddy_spawn"}, type = "boolean"}, + ["gameplay.enable_shared_spawns"] = {mod_key = "oarc-mod-enable-shared-spawns" , ocfg_keys = {"gameplay", "enable_shared_spawns"}, type = "boolean"}, + ["gameplay.number_of_players_per_shared_spawn"] = {mod_key = "oarc-mod-number-of-players-per-shared-spawn" , ocfg_keys = {"gameplay", "number_of_players_per_shared_spawn"}, type = "integer"}, + ["gameplay.default_surface"] = {mod_key = "oarc-mod-default-surface" , ocfg_keys = {"gameplay", "default_surface"}, type = "string"}, + ["gameplay.enable_secondary_spawns"] = {mod_key = "oarc-mod-enable-secondary-spawns" , ocfg_keys = {"gameplay", "enable_secondary_spawns"}, type = "boolean"}, + -- STARTUP ["gameplay.main_force_name"] = {mod_key = "oarc-mod-main-force-name" , ocfg_keys = {"gameplay", "main_force_name"}, type = "string"}, + + ["gameplay_difficulty_scaling_SUBHEADER"] = {mod_key = "" , ocfg_keys = {""}, type = "subheader", text = {"oarc-settings-section-subheader-difficulty-scaling"}}, + ["gameplay.enable_offline_protection"] = {mod_key = "oarc-mod-enable-offline-protection" , ocfg_keys = {"gameplay", "enable_offline_protection"}, type = "boolean"}, + ["gameplay.scale_resources_around_spawns"] = {mod_key = "oarc-mod-scale-resources-around-spawns" , ocfg_keys = {"gameplay", "scale_resources_around_spawns"}, type = "boolean"}, + ["gameplay.modified_enemy_spawning"] = {mod_key = "oarc-mod-modified-enemy-spawning" , ocfg_keys = {"gameplay", "modified_enemy_spawning"}, type = "boolean"}, + ["gameplay.modified_enemy_easy_evo"] = {mod_key = "oarc-mod-modified-enemy-easy-evo" , ocfg_keys = {"gameplay", "modified_enemy_easy_evo"}, type = "double"}, + ["gameplay.modified_enemy_medium_evo"] = {mod_key = "oarc-mod-modified-enemy-medium-evo" , ocfg_keys = {"gameplay", "modified_enemy_medium_evo"}, type = "double"}, + + ["gameplay_misc_SUBHEADER"] = {mod_key = "" , ocfg_keys = {""}, type = "subheader", text = {"oarc-settings-section-subheader-gameplay-misc"}}, + ["gameplay.enable_friendly_fire"] = {mod_key = "oarc-mod-enable-friendly-fire" , ocfg_keys = {"gameplay", "enable_friendly_fire"}, type = "boolean"}, + ["gameplay.minimum_online_time"] = {mod_key = "oarc-mod-minimum-online-time" , ocfg_keys = {"gameplay", "minimum_online_time"}, type = "integer"}, + ["gameplay.respawn_cooldown_min"] = {mod_key = "oarc-mod-respawn-cooldown-min" , ocfg_keys = {"gameplay", "respawn_cooldown_min"}, type = "integer"}, + + ["gameplay_sharing_SUBHEADER"] = {mod_key = "" , ocfg_keys = {""}, type = "subheader", text = {"oarc-settings-section-subheader-sharing"}}, + ["gameplay.enable_shared_team_vision"] = {mod_key = "oarc-mod-enable-shared-team-vision" , ocfg_keys = {"gameplay", "enable_shared_team_vision"}, type = "boolean"}, + ["gameplay.enable_shared_team_chat"] = {mod_key = "oarc-mod-enable-shared-team-chat" , ocfg_keys = {"gameplay", "enable_shared_team_chat"}, type = "boolean"}, + ["gameplay.enable_shared_power"] = {mod_key = "oarc-mod-enable-shared-power" , ocfg_keys = {"gameplay", "enable_shared_power"}, type = "boolean"}, + ["gameplay.enable_shared_chest"] = {mod_key = "oarc-mod-enable-shared-chest" , ocfg_keys = {"gameplay", "enable_shared_chest"}, type = "boolean"}, + + ["regrowth_HEADER"] = {mod_key = "" , ocfg_keys = {""}, type = "header", text = {"oarc-settings-section-header-regrowth"}}, + ["regrowth_SUBHEADER"] = {mod_key = "" , ocfg_keys = {""}, type = "subheader", text = {"oarc-settings-section-subheader-regrowth-warning"}}, + ["regrowth.enable_regrowth"] = {mod_key = "oarc-mod-enable-regrowth" , ocfg_keys = {"regrowth", "enable_regrowth"}, type = "boolean"}, + ["regrowth.enable_world_eater"] = {mod_key = "oarc-mod-enable-world-eater" , ocfg_keys = {"regrowth", "enable_world_eater"}, type = "boolean"}, + ["regrowth.enable_abandoned_base_cleanup"] = {mod_key = "oarc-mod-enable-abandoned-base-cleanup" , ocfg_keys = {"regrowth", "enable_abandoned_base_cleanup"}, type = "boolean"}, + ["regrowth.cleanup_interval"] = {mod_key = "oarc-mod-regrowth-cleanup-interval-min" , ocfg_keys = {"regrowth", "cleanup_interval"}, type = "integer"}, + + ["general_spawn_HEADER"] = {mod_key = "" , ocfg_keys = {""}, type = "header", text = {"oarc-settings-section-header-general-spawn"}}, + ["spawn_general.spawn_radius_tiles"] = {mod_key = "oarc-mod-spawn-general-radius-tiles" , ocfg_keys = {"spawn_general", "spawn_radius_tiles"}, type = "integer"}, + ["spawn_general.moat_width_tiles"] = {mod_key = "oarc-mod-spawn-general-moat-width-tiles" , ocfg_keys = {"spawn_general", "moat_width_tiles"}, type = "integer"}, + ["spawn_general.tree_width_tiles"] = {mod_key = "oarc-mod-spawn-general-tree-width-tiles" , ocfg_keys = {"spawn_general", "tree_width_tiles"}, type = "integer"}, + ["spawn_general.resources_shape"] = {mod_key = "oarc-mod-spawn-general-enable-resources-circle-shape" , ocfg_keys = {"spawn_general", "resources_shape"}, type = "string-list"}, + ["spawn_general.force_grass"] = {mod_key = "oarc-mod-spawn-general-enable-force-grass" , ocfg_keys = {"spawn_general", "force_grass"}, type = "boolean"}, + ["spawn_general.shape"] = {mod_key = "oarc-mod-spawn-general-shape" , ocfg_keys = {"spawn_general", "shape"}, type = "string-list"}, + + ["resource_placement_HEADER"] = {mod_key = "" , ocfg_keys = {""}, type = "header", text = {"oarc-settings-section-header-resource-placement"}}, + ["resource_placement.enabled"] = {mod_key = "oarc-mod-resource-placement-enabled" , ocfg_keys = {"resource_placement", "enabled"}, type = "boolean"}, + ["resource_placement.size_multiplier"] = {mod_key = "oarc-mod-resource-placement-size-multiplier" , ocfg_keys = {"resource_placement", "size_multiplier"}, type = "double"}, + ["resource_placement.amount_multiplier"] = {mod_key = "oarc-mod-resource-placement-amount-multiplier" , ocfg_keys = {"resource_placement", "amount_multiplier"}, type = "double"}, + + ["resource_placement_circle_SUBHEADER"] = {mod_key = "" , ocfg_keys = {""}, type = "subheader", text = {"oarc-settings-section-subheader-resource-placement-circular"}}, + ["resource_placement.distance_to_edge"] = {mod_key = "oarc-mod-resource-placement-distance-to-edge" , ocfg_keys = {"resource_placement", "distance_to_edge"}, type = "integer"}, + ["resource_placement.angle_offset"] = {mod_key = "oarc-mod-resource-placement-angle-offset" , ocfg_keys = {"resource_placement", "angle_offset"}, type = "double"}, + ["resource_placement.angle_final"] = {mod_key = "oarc-mod-resource-placement-angle-final" , ocfg_keys = {"resource_placement", "angle_final"}, type = "double"}, + + ["resource_placement_square_SUBHEADER"] = {mod_key = "" , ocfg_keys = {""}, type = "subheader", text = {"oarc-settings-section-subheader-resource-placement-square"}}, + ["resource_placement.vertical_offset"] = {mod_key = "oarc-mod-resource-placement-vertical-offset" , ocfg_keys = {"resource_placement", "vertical_offset"}, type = "integer"}, + ["resource_placement.horizontal_offset"] = {mod_key = "oarc-mod-resource-placement-horizontal-offset" , ocfg_keys = {"resource_placement", "horizontal_offset"}, type = "integer"}, + ["resource_placement.linear_spacing"] = {mod_key = "oarc-mod-resource-placement-linear-spacing" , ocfg_keys = {"resource_placement", "linear_spacing"}, type = "integer"}, +} + +---Easy reverse lookup for mod settings keys. +---@type table +OCFG_MOD_KEYS = +{ + ["oarc-mod-welcome-msg-title"] = "server_info.welcome_msg_title", + ["oarc-mod-welcome-msg"] = "server_info.welcome_msg", + ["oarc-mod-discord-invite"] = "server_info.discord_invite", + + ["oarc-mod-enable-main-team"] = "gameplay.enable_main_team", + ["oarc-mod-enable-separate-teams"] = "gameplay.enable_separate_teams", + -- STARTUP ["oarc-mod-default-allow-spawning-on-other-surfaces"] = " ["gameplay.enable_spawning_on_other_surfaces", + ["oarc-mod-allow-moats-around-spawns"] = "gameplay.allow_moats_around_spawns", + ["oarc-mod-enable-moat-bridging"] = "gameplay.enable_moat_bridging", + ["oarc-mod-minimum-distance-to-existing-chunks"] = "gameplay.minimum_distance_to_existing_chunks", + ["oarc-mod-near-spawn-distance"] = "gameplay.near_spawn_distance", + ["oarc-mod-far-spawn-distance"] = "gameplay.far_spawn_distance", + + ["oarc-mod-enable-buddy-spawn"] = "gameplay.enable_buddy_spawn", + ["oarc-mod-enable-offline-protection"] = "gameplay.enable_offline_protection", + ["oarc-mod-enable-shared-team-vision"] = "gameplay.enable_shared_team_vision", + ["oarc-mod-enable-shared-team-chat"] = "gameplay.enable_shared_team_chat", + ["oarc-mod-enable-shared-spawns"] = "gameplay.enable_shared_spawns", + ["oarc-mod-number-of-players-per-shared-spawn"] = "gameplay.number_of_players_per_shared_spawn", + ["oarc-mod-enable-friendly-fire"] = "gameplay.enable_friendly_fire", + + -- STARTUP ["oarc-mod-main-force-name"] = "gameplay.main_force_name", + ["oarc-mod-default-surface"] = "gameplay.default_surface", + ["oarc-mod-enable-secondary-spawns"] = "gameplay.enable_secondary_spawns", + + ["oarc-mod-scale-resources-around-spawns"] = "gameplay.scale_resources_around_spawns", + ["oarc-mod-modified-enemy-spawning"] = "gameplay.modified_enemy_spawning", + ["oarc-mod-modified-enemy-easy-evo"] = "gameplay.modified_enemy_easy_evo", + ["oarc-mod-modified-enemy-medium-evo"] = "gameplay.modified_enemy_medium_evo", + + ["oarc-mod-minimum-online-time"] = "gameplay.minimum_online_time", + ["oarc-mod-respawn-cooldown-min"] = "gameplay.respawn_cooldown_min", + ["oarc-mod-enable-shared-power"] = "gameplay.enable_shared_power", + ["oarc-mod-enable-shared-chest"] = "gameplay.enable_shared_chest", + + ["oarc-mod-enable-regrowth"] = "regrowth.enable_regrowth", + ["oarc-mod-enable-world-eater"] = "regrowth.enable_world_eater", + ["oarc-mod-enable-abandoned-base-cleanup"] = "regrowth.enable_abandoned_base_cleanup", + ["oarc-mod-regrowth-cleanup-interval-min"] = "regrowth.cleanup_interval", + + ["oarc-mod-spawn-general-radius-tiles"] = "spawn_general.spawn_radius_tiles", + ["oarc-mod-spawn-general-moat-width-tiles"] = "spawn_general.moat_width_tiles", + ["oarc-mod-spawn-general-tree-width-tiles"] = "spawn_general.tree_width_tiles", + ["oarc-mod-spawn-general-enable-resources-circle-shape"] = "spawn_general.resources_shape", + ["oarc-mod-spawn-general-enable-force-grass"] = "spawn_general.force_grass", + ["oarc-mod-spawn-general-shape"] = "spawn_general.shape", + + ["oarc-mod-resource-placement-enabled"] = "resource_placement.enabled", + ["oarc-mod-resource-placement-distance-to-edge"] = "resource_placement.distance_to_edge", + ["oarc-mod-resource-placement-angle-offset"] = "resource_placement.angle_offset", + ["oarc-mod-resource-placement-angle-final"] = "resource_placement.angle_final", + ["oarc-mod-resource-placement-vertical-offset"] = "resource_placement.vertical_offset", + ["oarc-mod-resource-placement-horizontal-offset"] = "resource_placement.horizontal_offset", + ["oarc-mod-resource-placement-linear-spacing"] = "resource_placement.linear_spacing", + ["oarc-mod-resource-placement-size-multiplier"] = "resource_placement.size_multiplier", + ["oarc-mod-resource-placement-amount-multiplier"] = "resource_placement.amount_multiplier" +} + + +function ValidateAndLoadConfig() + + -- Check that each of the OCFG_MOD_KEYS has a corresponding OCFG_KEYS entry. + for mod_key,ocfg_key in pairs(OCFG_MOD_KEYS) do + if (OCFG_KEYS[ocfg_key] == nil) then + error("OCFG_MOD_KEYS entry does not have a corresponding OCFG_KEYS entry: " .. mod_key .. " -> " .. ocfg_key) + end + end + -- And check the opposite. + for ocfg_key,entry in pairs(OCFG_KEYS) do + if (entry.type ~= "header") and (entry.type ~= "subheader") and (OCFG_MOD_KEYS[entry.mod_key] == nil) then + error("OCFG_KEYS entry does not have a corresponding OCFG_MOD_KEYS entry: " .. ocfg_key .. " -> " .. entry.mod_key) + end + end + + -- Load the template config into the global table. + ---@class OarcConfig + global.ocfg = OCFG + + -- Check that each entry in OCFG matches the default value of the mod setting. This is just for my own sanity. + -- Helps make sure mod default settings and my internal config are in sync. + for _,entry in pairs(OCFG_KEYS) do + if (entry.type ~= "header") and (entry.type ~= "subheader") then + local mod_key = entry.mod_key + local oarc_key = entry.ocfg_keys + local mod_value = game.mod_setting_prototypes[mod_key].default_value + local oarc_value = GetGlobalOarcConfigUsingKeyTable(oarc_key) + if (mod_value ~= oarc_value) then + error("OCFG value does not match mod setting: " .. mod_key .. " = " .. tostring(mod_value) .. " -> " .. serpent.block(oarc_key) .. " = " .. tostring(oarc_value)) + end + end + end + + CacheModSettings() -- Get all mod settings and overwrite the defaults in OARC_CFG. + + GetScenarioOverrideSettings() -- Get any scenario settings and overwrite both the mod settings and OARC_CFG. + + ValidateSettings() -- These are validation checks that can't be done within the mod settings natively. +end + +function ValidateSettings() + + -- Validate enable_main_team and enable_separate_teams. + -- Force enable_main_team if both are disabled. + if (not global.ocfg.gameplay.enable_main_team and not global.ocfg.gameplay.enable_separate_teams) then + log("Both main force and separate teams are disabled! Enabling main force. Please check your mod settings or config!") + global.ocfg.gameplay.enable_main_team = true + settings.global["oarc-mod-enable-main-team"] = { value = true } + SendBroadcastMsg("Invalid setting! Both main force and separate teams are disabled! Enabling main force.") + end + + -- Validate minimum is less than maximums + if (global.ocfg.gameplay.near_spawn_distance >= global.ocfg.gameplay.far_spawn_distance) then + log("Near spawn min distance is greater than or equal to near spawn max distance! Please check your mod settings or config!") + global.ocfg.gameplay.far_spawn_distance = global.ocfg.gameplay.near_spawn_distance + 1 + settings.global["oarc-mod-far-spawn-distance"] = { value = global.ocfg.gameplay.far_spawn_distance } + SendBroadcastMsg("Invalid setting! Near spawn min distance is greater than or equal to near spawn max distance!") + end + + -- Validate that regrowth is enabled if world eater is enabled. + if (global.ocfg.regrowth.enable_world_eater and not global.ocfg.regrowth.enable_regrowth) then + log("World eater is enabled but regrowth is not! Disabling world eater. Please check your mod settings or config!") + global.ocfg.regrowth.enable_world_eater = false + settings.global["oarc-mod-enable-world-eater"] = { value = false } + SendBroadcastMsg("Invalid setting! World eater is enabled but regrowth is not! Disabling world eater.") + end + + -- Validate that default surface exists. + if (game.surfaces[global.ocfg.gameplay.default_surface] == nil) then + log("Default surface does not exist! Please check your mod settings or config!") + global.ocfg.gameplay.default_surface = "nauvis" + settings.global["oarc-mod-default-surface"] = { value = "nauvis" } + SendBroadcastMsg("Invalid setting! Default surface does not exist! Setting to nauvis.") + end + + -- Validate that a "nauvis" surface config exists (nauvis is the default config fallback) + -- This should only break with a bad scenario custom config. + if (global.ocfg.surfaces_config["nauvis"] == nil) then + error("nauvis surface config does not exist! Please check your mod settings or config!") + end +end + +-- Read in the mod settings and copy them to the OARC_CFG table, overwriting the defaults in config.lua. +function CacheModSettings() + + log("Copying mod settings to OCFG table...") + + -- Copy the global settings from the mod settings. + -- Find the matching OARC setting and update it. + for _,entry in pairs(OCFG_KEYS) do + if (entry.type ~= "header") and (entry.type ~= "subheader") then + SetGlobalOarcConfigUsingKeyTable(entry.ocfg_keys, settings.global[entry.mod_key].value) + end + end + + -- Special case for startup settings + global.ocfg.gameplay.default_allow_spawning_on_other_surfaces = settings.startup["oarc-mod-default-allow-spawning-on-other-surfaces"].value --[[@as boolean]] + global.ocfg.gameplay.main_force_name = settings.startup["oarc-mod-main-force-name"].value --[[@as string]] +end + +function GetScenarioOverrideSettings() + + if remote.interfaces["oarc_scenario"] then + + log("Getting scenario override settings...") + local scenario_settings = remote.call("oarc_scenario", "get_scenario_settings") + + -- Overwrite the non mod settings with the scenario settings. + global.ocfg = scenario_settings + + -- Override the mod settings with the scenario settings! + for _,entry in pairs(OCFG_KEYS) do + if (entry.type ~= "header") and (entry.type ~= "subheader") then + local mod_key = entry.mod_key + local oarc_key = entry.ocfg_keys + local scenario_value = GetGlobalOarcConfigUsingKeyTable(oarc_key) + if (scenario_value ~= nil) then + local ok,result = pcall(function() settings.global[mod_key] = { value = scenario_value } end) + if not ok then + error("Error setting mod setting: " .. mod_key .. " = " .. tostring(scenario_value) .. "\n" .. "If you see this, you probably picked an invalid value for a setting override in the custom scenario.") + end + end + end + end + + else + log("No scenario settings found.") + end +end + +---Handles the event when a mod setting is changed in the mod settings menu. +---@param event EventData.on_runtime_mod_setting_changed +---@return nil +function RuntimeModSettingChanged(event) + + if (event.setting_type ~= "runtime-global") then + return + end + + log("on_runtime_mod_setting_changed: " .. event.setting .. " = " .. tostring(settings.global[event.setting].value)) + + -- Find the matching OARC setting and update it. + local found_setting = false + + if (OCFG_MOD_KEYS[event.setting] ~= nil) then + local oarc_setting_index = OCFG_MOD_KEYS[event.setting] + local oarc_setting_table = OCFG_KEYS[oarc_setting_index] + SetGlobalOarcConfigUsingKeyTable(oarc_setting_table.ocfg_keys, settings.global[event.setting].value) + found_setting = true + end + + if (not found_setting) then + error("Unknown oarc-mod setting changed: " .. event.setting) + else + ValidateSettings() + ApplyRuntimeChanges(OCFG_MOD_KEYS[event.setting]) + end +end + +---A probably quit stupid function to let me lookup and set the global.ocfg entries using a key table. +---@param key_table table +---@param value any +function SetGlobalOarcConfigUsingKeyTable(key_table, value) + local number_of_keys = #key_table + + if (number_of_keys == 1) then + global.ocfg[key_table[1]] = value + elseif (number_of_keys == 2) then + global.ocfg[key_table[1]][key_table[2]] = value + elseif (number_of_keys == 3) then + global.ocfg[key_table[1]][key_table[2]][key_table[3]] = value + else + error("Invalid key_table length: " .. number_of_keys .. "\n" .. serpent.block(key_table)) + end +end + +---An equally stupid function to let me lookup the global.ocfg entries using a key table. +---@param key_table table +---@return any +function GetGlobalOarcConfigUsingKeyTable(key_table) + local number_of_keys = #key_table + + if (number_of_keys == 1) then + if (global.ocfg[key_table[1]] == nil) then + error("Invalid key_table 1: " .. serpent.block(key_table)) + end + return global.ocfg[key_table[1]] + elseif (number_of_keys == 2) then + if (global.ocfg[key_table[1]] == nil) or (global.ocfg[key_table[1]][key_table[2]] == nil) then + error("Invalid key_table 2: " .. serpent.block(key_table)) + end + return global.ocfg[key_table[1]][key_table[2]] + elseif (number_of_keys == 3) then + if (global.ocfg[key_table[1]] == nil) or + (global.ocfg[key_table[1]][key_table[2]] == nil) or + (global.ocfg[key_table[1]][key_table[2]][key_table[3]] == nil) then + error("Invalid key_table 3: " .. serpent.block(key_table)) + end + return global.ocfg[key_table[1]][key_table[2]][key_table[3]] + else + error("Invalid key_table length: " .. number_of_keys .. "\n" .. serpent.block(key_table)) + end +end + +---Handles any runtime changes that need more than just the setting change. +---@param oarc_setting_index string +---@return nil +function ApplyRuntimeChanges(oarc_setting_index) + + ---Handle changing enable_shared_team_vision + if (oarc_setting_index == "gameplay.enable_shared_team_vision") then + for _,force in pairs(game.forces) do + if (force.name ~= "neutral") and (force.name ~= "enemy") and (force.name ~= "enemy-easy") then + force.share_chart = global.ocfg.gameplay.enable_shared_team_vision + end + end + + ---Handle changing enable_friendly_fire + elseif (oarc_setting_index == "gameplay.enable_friendly_fire") then + for _,force in pairs(game.forces) do + if (force.name ~= "neutral") and (force.name ~= "enemy") and (force.name ~= "enemy-easy") then + force.friendly_fire = global.ocfg.gameplay.enable_friendly_fire + end + end + + end +end \ No newline at end of file diff --git a/lib/frontier_silo.lua b/lib/frontier_silo.lua deleted file mode 100644 index d40b457..0000000 --- a/lib/frontier_silo.lua +++ /dev/null @@ -1,401 +0,0 @@ --- frontier_silo.lua --- Jan 2018 --- My take on frontier silos for my Oarc scenario - -require("config") -require("lib/oarc_utils") - --------------------------------------------------------------------------------- --- Frontier style rocket silo stuff --------------------------------------------------------------------------------- - - -function SpawnSilosAndGenerateSiloAreas() - - -- Special silo islands mode "boogaloo" - if (global.ocfg.silo_islands) then - - local num_spawns = #global.vanillaSpawns - local new_spawn_list = {} - - -- Pick out every OTHER vanilla spawn for the rocket silos. - for k,v in pairs(global.vanillaSpawns) do - if ((k <= num_spawns/2) and (k%2==1)) then - SetFixedSiloPosition({x=v.x,y=v.y}) - elseif ((k > num_spawns/2) and (k%2==0)) then - SetFixedSiloPosition({x=v.x,y=v.y}) - else - table.insert(new_spawn_list, v) - end - end - global.vanillaSpawns = new_spawn_list - - -- A set of fixed silo positions - elseif (global.ocfg.frontier_fixed_pos) then - for k,v in pairs(global.ocfg.frontier_pos_table) do - SetFixedSiloPosition(v) - end - - -- Random locations on a circle. - else - SetRandomSiloPosition(global.ocfg.frontier_silo_count) - - end - - -- Freezes the game at the start to generate all the chunks. - GenerateRocketSiloAreas(game.surfaces[GAME_SURFACE_NAME]) -end - --- This creates a random silo position, stored to global.siloPosition --- It uses the config setting global.ocfg.frontier_silo_distance and spawns the --- silo somewhere on a circle edge with radius using that distance. -function SetRandomSiloPosition(num_silos) - if (global.siloPosition == nil) then - global.siloPosition = {} - end - - local random_angle_offset = math.random(0, math.pi * 2) - - for i=1,num_silos do - local theta = ((math.pi * 2) / num_silos); - local angle = (theta * i) + random_angle_offset; - - local tx = (global.ocfg.frontier_silo_distance*CHUNK_SIZE * math.cos(angle)) - local ty = (global.ocfg.frontier_silo_distance*CHUNK_SIZE * math.sin(angle)) - - -- Ensure it's centered around a chunk - local tx = (tx - (tx % CHUNK_SIZE)) + CHUNK_SIZE/2 - local ty = (ty - (ty % CHUNK_SIZE)) + CHUNK_SIZE/2 - - table.insert(global.siloPosition, {x=math.floor(tx), y=math.floor(ty)}) - - log("Silo position: " .. tx .. ", " .. ty .. ", " .. angle) - end -end - --- Sets the global.siloPosition var to the set in the config file -function SetFixedSiloPosition(pos) - table.insert(global.siloPosition, pos) -end - --- Create a rocket silo at the specified positionmmmm --- Also makes sure tiles and entities are cleared if required. -local function CreateRocketSilo(surface, siloPosition, force) - - -- Delete any entities beneath the silo? - for _, entity in pairs(surface.find_entities_filtered{area = {{siloPosition.x-5, - siloPosition.y-6}, - {siloPosition.x+6, - siloPosition.y+6}}}) do - entity.destroy() - end - - -- Remove nearby enemies again - for _, entity in pairs(surface.find_entities_filtered{area = {{siloPosition.x-(CHUNK_SIZE*4), - siloPosition.y-(CHUNK_SIZE*4)}, - {siloPosition.x+(CHUNK_SIZE*4), - siloPosition.y+(CHUNK_SIZE*4)}}, force = "enemy"}) do - entity.destroy() - end - - -- Set tiles below the silo - tiles = {} - for dx = -6,5 do - for dy = -6,5 do - if (game.active_mods["oarc-restricted-build"]) then - table.insert(tiles, {name = global.ocfg.locked_build_area_tile, - position = {siloPosition.x+dx, siloPosition.y+dy}}) - else - if ((dx % 2 == 0) or (dx % 2 == 0)) then - table.insert(tiles, {name = "concrete", - position = {siloPosition.x+dx, siloPosition.y+dy}}) - else - table.insert(tiles, {name = "hazard-concrete-left", - position = {siloPosition.x+dx, siloPosition.y+dy}}) - end - end - end - end - surface.set_tiles(tiles, true) - - -- Create indestructible silo and assign to a force - if not global.ocfg.frontier_allow_build then - local silo = surface.create_entity{name = "rocket-silo", position = {siloPosition.x+0.5, siloPosition.y}, force = force} - silo.destructible = false - silo.minable = false - end - - -- TAG it on the main force at least. - game.forces[global.ocfg.main_force].add_chart_tag(game.surfaces[GAME_SURFACE_NAME], - {position=siloPosition, text="Rocket Silo", - icon={type="item",name="rocket-silo"}}) - - -- Make silo safe from being removed. - if global.ocfg.enable_regrowth then - RegrowthMarkAreaSafeGivenTilePos(siloPosition, 5, true) - end - - if ENABLE_SILO_BEACONS then - PhilipsBeacons(surface, siloPosition, game.forces[global.ocfg.main_force]) - end - if ENABLE_SILO_RADAR then - PhilipsRadar(surface, siloPosition, game.forces[global.ocfg.main_force]) - end - -end - --- Generates all rocket silos, should be called after the areas are generated --- Includes a crop circle -function GenerateAllSilos() - - -- Create each silo in the list - for idx,siloPos in pairs(global.siloPosition) do - CreateRocketSilo(game.surfaces[GAME_SURFACE_NAME], siloPos, global.ocfg.main_force) - end -end - --- Validates any attempt to build a silo. --- Should be call in on_built_entity and on_robot_built_entity -function BuildSiloAttempt(event) - - -- Validation - if (event.created_entity == nil) then return end - - local e_name = event.created_entity.name - if (event.created_entity.name == "entity-ghost") then - e_name =event.created_entity.ghost_name - end - - if (e_name ~= "rocket-silo") then return end - - -- Check if it's in the right area. - local epos = event.created_entity.position - - for k,v in pairs(global.siloPosition) do - if (getDistance(epos, v) <= 1) then - if (event.created_entity.name ~= "entity-ghost") then - SendBroadcastMsg("Rocket silo has been built!") - end - return -- THIS MEANS WE SUCCESFULLY BUILT THE SILO (ghost or actual building.) - end - end - - -- If we get here, means it wasn't in a valid position. Need to remove it. - if (event.created_entity.last_user ~= nil) then - FlyingText("Can't build silo here! Check the map!", epos, my_color_red, event.created_entity.surface) - if (event.created_entity.name == "entity-ghost") then - event.created_entity.destroy() - else - event.created_entity.last_user.mine_entity(event.created_entity, true) - end - else - log("ERROR! Rocket-silo had no valid last user?!?!") - end -end - --- Generate clean land and trees around silo area -function GenerateRocketSiloChunks() - - -- Silo generation can take awhile depending on the number of silos. - -- if (game.tick < #global.siloPosition*10*TICKS_PER_SECOND) then - local surface = game.surfaces[GAME_SURFACE_NAME] - -- local chunkArea = event.area - - -- local chunkAreaCenter = {x=chunkArea.left_top.x+(CHUNK_SIZE/2), - -- y=chunkArea.left_top.y+(CHUNK_SIZE/2)} - - for _,siloPos in pairs(global.siloPosition) do - local siloArea = {left_top= - {x=siloPos.x-(CHUNK_SIZE*2), - y=siloPos.y-(CHUNK_SIZE*2)}, - right_bottom= - {x=siloPos.x+(CHUNK_SIZE*2), - y=siloPos.y+(CHUNK_SIZE*2)}} - - - -- Clear enemies directly next to the rocket - -- if CheckIfInArea(chunkAreaCenter,siloArea) then - for _, entity in pairs(surface.find_entities_filtered{area = siloArea, force = "enemy"}) do - entity.destroy() - end - - -- Remove trees/resources inside the spawn area - RemoveInCircle(surface, siloArea, "tree", siloPos, (CHUNK_SIZE*1.5)+5) - RemoveInCircle(surface, siloArea, "resource", siloPos, (CHUNK_SIZE*1.5)+5) - RemoveInCircle(surface, siloArea, "cliff", siloPos, (CHUNK_SIZE*1.5)+5) - RemoveDecorationsArea(surface, siloArea) - - -- Create rocket silo - CreateCropOctagon(surface, siloPos, siloArea, (CHUNK_SIZE*1.5)+4, "landfill") - -- end - end - -- end -end - --- Generate chunks where we plan to place the rocket silos. -function GenerateRocketSiloAreas(surface) - for idx,siloPos in pairs(global.siloPosition) do - surface.request_to_generate_chunks({siloPos.x, siloPos.y}, 1) - end - if (global.ocfg.frontier_silo_vision) then - ChartRocketSiloAreas(surface, game.forces[global.ocfg.main_force]) - end - - game.surfaces[GAME_SURFACE_NAME].force_generate_chunk_requests() -- Block and generate all to be sure. - - GenerateRocketSiloChunks() - GenerateAllSilos() -end - --- Chart chunks where we plan to place the rocket silos. -function ChartRocketSiloAreas(surface, force) - for idx,siloPos in pairs(global.siloPosition) do - force.chart(surface, {{siloPos.x-(CHUNK_SIZE*1), - siloPos.y-(CHUNK_SIZE*1)}, - {siloPos.x+(CHUNK_SIZE*1), - siloPos.y+(CHUNK_SIZE*1)}}) - end -end - -function PhilipsBeacons(surface, siloPos, force) - - -- Add Beacons - -- x = right, left; y = up, down - -- top 1 left 1 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x-8, siloPos.y-8}, force = force} - beacon.destructible = false - beacon.minable = false - -- top 2 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x-5, siloPos.y-8}, force = force} - beacon.destructible = false - beacon.minable = false - -- top 3 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x-2, siloPos.y-8}, force = force} - beacon.destructible = false - beacon.minable = false - -- top 4 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x+2, siloPos.y-8}, force = force} - beacon.destructible = false - beacon.minable = false - -- top 5 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x+5, siloPos.y-8}, force = force} - beacon.destructible = false - beacon.minable = false - -- top 6 right 1 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x+8, siloPos.y-8}, force = force} - beacon.destructible = false - beacon.minable = false - -- left 2 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x-8, siloPos.y-5}, force = force} - beacon.destructible = false - beacon.minable = false - -- left 3 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x-8, siloPos.y-2}, force = force} - beacon.destructible = false - beacon.minable = false - -- left 4 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x-8, siloPos.y+2}, force = force} - beacon.destructible = false - beacon.minable = false - -- left 5 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x-8, siloPos.y+5}, force = force} - beacon.destructible = false - beacon.minable = false - -- left 6 bottom 1 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x-8, siloPos.y+8}, force = force} - beacon.destructible = false - beacon.minable = false - -- left 7 bottom 2 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x-5, siloPos.y+8}, force = force} - beacon.destructible = false - beacon.minable = false - -- right 2 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x+8, siloPos.y-5}, force = force} - beacon.destructible = false - beacon.minable = false - -- right 3 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x+8, siloPos.y-2}, force = force} - beacon.destructible = false - beacon.minable = false - -- right 4 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x+8, siloPos.y+2}, force = force} - beacon.destructible = false - beacon.minable = false - -- right 5 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x+8, siloPos.y+5}, force = force} - beacon.destructible = false - beacon.minable = false - -- right 6 bottom 3 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x+5, siloPos.y+8}, force = force} - beacon.destructible = false - beacon.minable = false - -- right 7 bottom 4 - local beacon = surface.create_entity{name = "beacon", position = {siloPos.x+8, siloPos.y+8}, force = force} - beacon.destructible = false - beacon.minable = false - -- substations - -- top left - local substation = surface.create_entity{name = "substation", position = {siloPos.x-5, siloPos.y-5}, force = force} - substation.destructible = false - substation.minable = false - -- top right - local substation = surface.create_entity{name = "substation", position = {siloPos.x+6, siloPos.y-5}, force = force} - substation.destructible = false - substation.minable = false - -- bottom left - local substation = surface.create_entity{name = "substation", position = {siloPos.x-5, siloPos.y+6}, force = force} - substation.destructible = false - substation.minable = false - -- bottom right - local substation = surface.create_entity{name = "substation", position = {siloPos.x+6, siloPos.y+6}, force = force} - substation.destructible = false - substation.minable = false - - -- end adding beacons -end - -function PhilipsRadar(surface, siloPos, force) - - local radar = surface.create_entity{name = "solar-panel", position = {siloPos.x-43, siloPos.y+3}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "solar-panel", position = {siloPos.x-43, siloPos.y-3}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "solar-panel", position = {siloPos.x-40, siloPos.y-6}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "solar-panel", position = {siloPos.x-37, siloPos.y-6}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "solar-panel", position = {siloPos.x-34, siloPos.y-6}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "solar-panel", position = {siloPos.x-34, siloPos.y-3}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "solar-panel", position = {siloPos.x-34, siloPos.y}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "solar-panel", position = {siloPos.x-34, siloPos.y+3}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "solar-panel", position = {siloPos.x-43, siloPos.y-6}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "solar-panel", position = {siloPos.x-40, siloPos.y+3}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "solar-panel", position = {siloPos.x-37, siloPos.y+3}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "radar", position = {siloPos.x-43, siloPos.y}, force = force} - radar.destructible = false - local substation = surface.create_entity{name = "substation", position = {siloPos.x-38, siloPos.y-1}, force = force} - substation.destructible = false - local radar = surface.create_entity{name = "accumulator", position = {siloPos.x-40, siloPos.y-1}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "accumulator", position = {siloPos.x-40, siloPos.y-3}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "accumulator", position = {siloPos.x-40, siloPos.y+1}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "accumulator", position = {siloPos.x-38, siloPos.y-3}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "accumulator", position = {siloPos.x-38, siloPos.y+1}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "accumulator", position = {siloPos.x-36, siloPos.y-1}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "accumulator", position = {siloPos.x-36, siloPos.y-3}, force = force} - radar.destructible = false - local radar = surface.create_entity{name = "accumulator", position = {siloPos.x-36, siloPos.y+1}, force = force} - radar.destructible = false -end \ No newline at end of file diff --git a/lib/game_opts.lua b/lib/game_opts.lua deleted file mode 100644 index 547aee1..0000000 --- a/lib/game_opts.lua +++ /dev/null @@ -1,160 +0,0 @@ --- game_opts.lua --- Jan 2018 --- Display current game options, maybe have some admin controls here - --- Main Configuration File -require("config") -require("lib/oarc_utils") -require("lib/separate_spawns") - -function GameOptionsGuiClick(event) - if not (event and event.element and event.element.valid) then return end - local player = game.players[event.player_index] - local name = event.element.name - - if (name == "ban_player") then - local pIndex = event.element.parent.ban_players_dropdown.selected_index - - if (pIndex ~= 0) then - local banPlayer = event.element.parent.ban_players_dropdown.get_item(pIndex) - if (game.players[banPlayer]) then - game.ban_player(banPlayer, "Banned from admin panel.") - log("Banning " .. banPlayer) - end - end - end - - if (name == "restart_player") then - local pIndex = event.element.parent.ban_players_dropdown.selected_index - - if (pIndex ~= 0) then - local resetPlayer = event.element.parent.ban_players_dropdown.get_item(pIndex) - if (game.players[resetPlayer]) then - RemoveOrResetPlayer(player, false, true, true, true) - SeparateSpawnsPlayerCreated(resetPlayer, true) - log("Resetting " .. resetPlayer) - end - end - end -end - --- Used by AddOarcGuiTab -function CreateGameOptionsTab(tab_container, player) - - if global.oarc_announcements ~= nil then - AddLabel(tab_container, "announcement_info_label", "Server announcements:", my_label_header_style) - AddLabel(tab_container, "announcement_info_txt", global.oarc_announcements, my_longer_label_style) - AddSpacerLine(tab_container) - end - - -- General Server Info: - AddLabel(tab_container, "info_1", global.ocfg.welcome_msg, my_longer_label_style) - AddLabel(tab_container, "info_2", global.ocfg.server_rules, my_longer_label_style) - AddLabel(tab_container, "info_3", global.ocfg.server_contact, my_longer_label_style) - tab_container.add{type="textfield", - tooltip="Come join the discord (copy this invite)!", - text=DISCORD_INV} - AddSpacerLine(tab_container) - - -- Enemy Settings: - local enemy_expansion_txt = "disabled" - if game.map_settings.enemy_expansion.enabled then enemy_expansion_txt = "enabled" end - - local enemy_text="Server Run Time: " .. formattime_hours_mins(game.tick) .. "\n" .. - "Current Evolution: " .. string.format("%.4f", game.forces["enemy"].evolution_factor) .. "\n" .. - "Enemy evolution time/pollution/destroy factors: " .. game.map_settings.enemy_evolution.time_factor .. "/" .. - game.map_settings.enemy_evolution.pollution_factor .. "/" .. - game.map_settings.enemy_evolution.destroy_factor .. "\n" .. - "Enemy expansion is " .. enemy_expansion_txt - - AddLabel(tab_container, "enemy_info", enemy_text, my_longer_label_style) - AddSpacerLine(tab_container) - - -- Soft Mods: - local soft_mods_string = "Oarc Core" - if (global.ocfg.enable_undecorator) then - soft_mods_string = soft_mods_string .. ", Undecorator" - end - if (global.ocfg.enable_tags) then - soft_mods_string = soft_mods_string .. ", Tags" - end - if (global.ocfg.enable_long_reach) then - soft_mods_string = soft_mods_string .. ", Long Reach" - end - if (global.ocfg.enable_autofill) then - soft_mods_string = soft_mods_string .. ", Auto Fill" - end - if (global.ocfg.enable_player_list) then - soft_mods_string = soft_mods_string .. ", Player List" - end - if (global.ocfg.enable_regrowth) then - soft_mods_string = soft_mods_string .. ", Regrowth" - end - if (global.ocfg.enable_chest_sharing) then - soft_mods_string = soft_mods_string .. ", Item & Energy Sharing" - end - if (global.ocfg.enable_magic_factories) then - soft_mods_string = soft_mods_string .. ", Special Map Chunks" - end - if (global.ocfg.enable_offline_protect) then - soft_mods_string = soft_mods_string .. ", Offline Attack Inhibitor" - end - - local game_info_str = "Soft Mods: " .. soft_mods_string - - -- Spawn options: - if (global.ocfg.enable_separate_teams) then - game_info_str = game_info_str.."\n".."You are allowed to spawn on your own team (have your own research tree). All teams are friendly!" - end - if (global.ocfg.enable_vanilla_spawns) then - game_info_str = game_info_str.."\n".."You are spawned in a default style starting area." - else - game_info_str = game_info_str.."\n".."You are spawned with a fix set of starting resources." - if (global.ocfg.enable_buddy_spawn) then - game_info_str = game_info_str.."\n".."You can chose to spawn alongside a buddy if you spawn together at the same time." - end - end - if (global.ocfg.enable_shared_spawns) then - game_info_str = game_info_str.."\n".."Spawn hosts may choose to share their spawn and allow other players to join them." - end - if (global.ocfg.enable_separate_teams and global.ocfg.enable_shared_team_vision) then - game_info_str = game_info_str.."\n".."Everyone (all teams) have shared vision." - end - if (global.ocfg.frontier_rocket_silo) then - game_info_str = game_info_str.."\n".."Silos are only placeable in certain areas on the map!" - end - if (global.ocfg.enable_regrowth) then - game_info_str = game_info_str.."\n".."Old parts of the map will slowly be deleted over time (chunks without any player buildings)." - end - if (global.ocfg.enable_power_armor_start or global.ocfg.enable_modular_armor_start) then - game_info_str = game_info_str.."\n".."Quicker start enabled." - end - if (global.ocfg.lock_goodies_rocket_launch) then - game_info_str = game_info_str.."\n".."Some technologies and recipes are locked until you launch a rocket!" - end - - - - AddLabel(tab_container, "game_info_label", game_info_str, my_longer_label_style) - - if (global.ocfg.enable_abandoned_base_removal) then - AddLabel(tab_container, "leave_warning_msg", "If you leave within " .. global.ocfg.minimum_online_time .. " minutes of joining, your base and character will be deleted.", my_longer_label_style) - tab_container.leave_warning_msg.style.font_color=my_color_red - end - - -- Ending Spacer - AddSpacerLine(tab_container) - - -- ADMIN CONTROLS - if (player.admin) then - player_list = {} - for _,player in pairs(game.connected_players) do - table.insert(player_list, player.name) - end - tab_container.add{name = "ban_players_dropdown", - type = "drop-down", - items = player_list} - tab_container.add{name="ban_player", type="button", caption="Ban Player"} - tab_container.add{name="restart_player", type="button", caption="Restart Player"} - end -end \ No newline at end of file diff --git a/lib/gui_tabs/mod_info_faq.lua b/lib/gui_tabs/mod_info_faq.lua new file mode 100644 index 0000000..cc74128 --- /dev/null +++ b/lib/gui_tabs/mod_info_faq.lua @@ -0,0 +1,42 @@ +-- Contains the GUI for the regrowth controls tab. + +---Used by AddOarcGuiTab +---@param tab_container LuaGuiElement +---@param player LuaPlayer +---@return nil +function CreateModInfoTab(tab_container, player) + + local scroll_pane = tab_container.add { + type = "scroll-pane", + vertical_scroll_policy = "auto", + } + scroll_pane.style.maximal_height = GENERIC_GUI_MAX_HEIGHT + scroll_pane.style.padding = 5 + + AddLabel(scroll_pane, nil, "Mod Info & FAQ", my_label_header2_style) + AddSpacerLine(scroll_pane) + + CreateFAQEntry(scroll_pane, { "oarc-mod-faq-what-is-this-mod" }, { "oarc-mod-faq-what-is-this-mod-answer" }) + CreateFAQEntry(scroll_pane, { "oarc-mod-faq-other-surfaces" }, { "oarc-mod-faq-other-surfaces-answer" }) + CreateFAQEntry(scroll_pane, { "oarc-mod-faq-secondary-spawns" }, { "oarc-mod-faq-secondary-spawns-answer" }) + CreateFAQEntry(scroll_pane, { "oarc-mod-faq-what-are-teams" }, { "oarc-mod-faq-what-are-teams-answer" }) + CreateFAQEntry(scroll_pane, { "oarc-mod-faq-shared-spawn" }, { "oarc-mod-faq-shared-spawn-answer" }) + CreateFAQEntry(scroll_pane, { "oarc-mod-faq-buddy-spawn" }, { "oarc-mod-faq-buddy-spawn-answer" }) + CreateFAQEntry(scroll_pane, { "oarc-mod-faq-regrowth" }, { "oarc-mod-faq-regrowth-answer" }) + CreateFAQEntry(scroll_pane, { "oarc-mod-faq-cleanup-abandoned" }, { "oarc-mod-faq-cleanup-abandoned-answer" }) + CreateFAQEntry(scroll_pane, { "oarc-mod-faq-offline-protection" }, { "oarc-mod-faq-offline-protection-answer" }) + CreateFAQEntry(scroll_pane, { "oarc-mod-faq-shared-power" }, { "oarc-mod-faq-shared-power-answer" }) + CreateFAQEntry(scroll_pane, { "oarc-mod-faq-shared-chest" }, { "oarc-mod-faq-shared-chest-answer" }) + +end + +---Creates a FAQ entry in the tab +---@param container LuaGuiElement +---@param question LocalisedString +---@param answer LocalisedString +---@return nil +function CreateFAQEntry(container, question, answer) + AddLabel(container, nil, question, "caption_label") + AddLabel(container, nil, answer, my_longer_label_style) + AddSpacerLine(container) +end \ No newline at end of file diff --git a/lib/gui_tabs/server_info.lua b/lib/gui_tabs/server_info.lua new file mode 100644 index 0000000..24b53f1 --- /dev/null +++ b/lib/gui_tabs/server_info.lua @@ -0,0 +1,142 @@ +---Display current game options and server info, maybe have some admin controls here + +---Creates the content for the game settings used by AddOarcGuiTab +---@param tab_container LuaGuiElement +---@param player LuaPlayer +---@return nil +function CreateServerInfoTab(tab_container, player) + + -- General Server Info: + if (global.ocfg.server_info.welcome_msg ~= " ") then + AddLabel(tab_container, nil, {"oarc-server-info-tab-welcome-msg-title"}, "caption_label") + AddLabel(tab_container, nil, global.ocfg.server_info.welcome_msg, my_longer_label_style) + AddSpacerLine(tab_container) + end + + if (global.ocfg.server_info.discord_invite ~= " ") then + local horizontal_flow = tab_container.add{ + type="flow", direction="horizontal" + } + AddLabel(horizontal_flow, nil, {"oarc-server-info-tab-discord-invite"}, "caption_label") + horizontal_flow.add{ + type="textfield", + tooltip={"oarc-server-info-tab-discord-invite-tooltip"}, + text=global.ocfg.server_info.discord_invite + } + AddSpacerLine(tab_container) + end + + AddLabel(tab_container, nil, {"oarc-server-info-tab-map-info-label"}, "caption_label") + AddLabel(tab_container, nil, {"oarc-server-info-tab-server-run-time", FormatTimeHoursSecs(game.tick)}, my_label_style) + --TODO: Add more stuff here maybe? Like in the old version? + + if (global.ocfg.regrowth.enable_abandoned_base_cleanup) then + local label = AddLabel(tab_container, nil, {"oarc-server-info-leave-warning", global.ocfg.gameplay.minimum_online_time}, my_longer_label_style) + label.style.font_color=my_color_red + end + + -- Ending Spacer + AddSpacerLine(tab_container) + + -- ADMIN CONTROLS + if (player.admin) then + player_list = {} + for _, p in pairs(game.connected_players) do + table.insert(player_list, p.name) + end + + AddLabel(tab_container, nil, {"oarc-server-info-admin-controls"}, "caption_label") + + local horizontal_flow = tab_container.add{ + type="flow", direction="horizontal" + } + horizontal_flow.style.horizontally_stretchable = true + + AddLabel(horizontal_flow, nil, {"oarc-server-info-ban-select-player"}, my_label_style) + local drop_down = horizontal_flow.add{ + name = "ban_players_dropdown", + tags = { action = "oarc_server_info_tab", setting = "ban_players_dropdown" }, + type = "drop-down", + items = player_list + } + + -- If there is only one player, select it by default (for testing convenience) + if (#player_list == 1) then + drop_down.selected_index = 1 + end + + local dragger = horizontal_flow.add{ + type="empty-widget", + style="draggable_space_header" + } + dragger.style.horizontally_stretchable = true + + horizontal_flow.add{ + name="ban_player", + tags = { action = "oarc_server_info_tab", setting = "ban_player" }, + type="button", + caption={"oarc-server-info-button-ban-player"}, + style = "red_button" + } + horizontal_flow.add{ + name="restart_player", + tags = { action = "oarc_server_info_tab", setting = "restart_player" }, + type="button", + caption={"oarc-server-info-button-restart-player"}, + style = "red_button" + } + end +end + + +---Server info gui click event handler +---@param event EventData.on_gui_click +---@return nil +function ServerInfoGuiClick(event) + if not event.element.valid then return end + local player = game.players[event.player_index] + local tags = event.element.tags + + if (tags.action ~= "oarc_server_info_tab") then + return + end + + local player_dropdown = event.element.parent.ban_players_dropdown + + if (tags.setting == "ban_player") then + local pIndex = player_dropdown.selected_index + + if (pIndex ~= 0) then + local banPlayer = player_dropdown.get_item(pIndex) + if (game.players[banPlayer]) then + game.ban_player(banPlayer --[[@as string]], "Banned from admin panel by " .. player.name) + log("Banning " .. banPlayer) + end + end + end + + if (tags.setting == "restart_player") then + local pIndex = player_dropdown.selected_index + + if (pIndex ~= 0) then + local resetPlayer = player_dropdown.get_item(pIndex) + + if not game.players[resetPlayer] or not game.players[resetPlayer].connected then + SendMsg(player.name, {"oarc-player-not-found", resetPlayer}) + return + end + + if PlayerHasDelayedSpawn(resetPlayer--[[@as string]]) then + SendMsg(player.name, {"oarc-player-about-to-spawn", resetPlayer}) + return + end + + log("Resetting " .. resetPlayer) + RemoveOrResetPlayer(game.players[resetPlayer], false) + SeparateSpawnsInitPlayer(resetPlayer --[[@as string]]) + else + SendMsg(player.name, {"oarc-player-none-selected"}) + return + end + end +end \ No newline at end of file diff --git a/lib/gui_tabs/settings_controls.lua b/lib/gui_tabs/settings_controls.lua new file mode 100644 index 0000000..507f8b9 --- /dev/null +++ b/lib/gui_tabs/settings_controls.lua @@ -0,0 +1,505 @@ +-- Contains the GUI for the controlling various settings of the mod. + +---Creates the content in the tab used by AddOarcGuiTab +---@param tab_container LuaGuiElement +---@param player LuaPlayer +function CreateSettingsControlsTab(tab_container, player) + + if (player.admin) then + local label = AddLabel(tab_container, nil, { "oarc-settings-tab-admin-warning" }, my_warning_style) + + else + local label = AddLabel(tab_container, nil, { "oarc-settings-tab-player-warning" }, my_warning_style) + + end + + local label = AddLabel(tab_container, nil, { "oarc-settings-tab-description" }, my_label_style) + label.style.bottom_padding = 5 + + local flow = tab_container.add { type = "flow", direction = "horizontal", } + + local scroll_pane_left = flow.add { + type = "scroll-pane", + direction = "vertical", + vertical_scroll_policy = "always", + } + scroll_pane_left.style.maximal_height = GENERIC_GUI_MAX_HEIGHT + scroll_pane_left.style.padding = 5 + scroll_pane_left.style.right_margin = 2 + CreateModSettingsSection(scroll_pane_left, player) + + local scroll_pane_right = flow.add { + type = "scroll-pane", + direction = "vertical", + vertical_scroll_policy = "always", + } + scroll_pane_right.style.maximal_height = GENERIC_GUI_MAX_HEIGHT + scroll_pane_right.style.padding = 5 + scroll_pane_right.style.left_margin = 2 + CreateSurfaceSettingsSection(scroll_pane_right, player) +end + +---Create the content for the mod settings section +---@param container LuaGuiElement +---@param player LuaPlayer +---@return nil +function CreateModSettingsSection(container, player) + AddLabel(container, nil, { "oarc-settings-tab-title-mod-settings" }, my_label_header2_style) + + for index,entry in pairs(OCFG_KEYS) do + if (entry.type == "header") then + AddSpacerLine(container) + AddLabel(container, nil, entry.text, "caption_label") + elseif (entry.type == "subheader") then + AddLabel(container, nil, entry.text, "bold_label") + elseif (entry.type == "boolean") then + AddCheckboxSetting(container, index, entry, player.admin) + elseif (entry.type == "string") then + AddTextfieldSetting(container, index, entry, player.admin) + elseif (entry.type == "integer") then + AddIntegerSetting(container, index, entry, player.admin) + elseif (entry.type == "double") then + AddDoubleSetting(container, index, entry, player.admin) + elseif (entry.type == "string-list") then + AddStringListDropdownSetting(container, index, entry, player.admin) + end + end +end + +---Create the content for the surface settings section +---@param container LuaGuiElement +---@param player LuaPlayer +---@return nil +function CreateSurfaceSettingsSection(container, player) + AddLabel(container, nil, { "oarc-settings-tab-title-surface" }, my_label_header2_style) + + --- Create a table with 3 columns. Surface Name, Spawning Enabled, Regrowth Enabled + local surface_table = container.add { + type = "table", + name = "surface_table", + column_count = 3, + style = "bordered_table", + } + + --- Add the header row + AddLabel(surface_table, nil, {"oarc-settings-tab-surface-column-header"}, "caption_label") + AddLabel(surface_table, nil, {"oarc-settings-tab-surface-spawning-enabled"}, "caption_label") + AddLabel(surface_table, nil, {"oarc-settings-tab-surface-regrowth-enabled"}, "caption_label") + + --- Add the rows + for name, allowed in pairs(global.oarc_surfaces --[[@as table]]) do + AddLabel(surface_table, nil, name, my_label_style) + AddSurfaceCheckboxSetting(surface_table, name, "spawn_enabled", allowed, player.admin, + { "oarc-settings-tab-surface-checkbox-tooltip" }) + local regrowth_enabled = TableContains(global.rg.active_surfaces, name) + AddSurfaceCheckboxSetting(surface_table, name, "regrowth_enabled", regrowth_enabled, player.admin, + {"oarc-settings-tab-surface-regrowth-checkbox-tooltip"}) + end + +end + +---Handles the click event for the tab used by AddOarcGuiTab +---@param event EventData.on_gui_click +---@return nil +function SettingsControlsTabGuiClick(event) + if not (event.element.valid) then return end + + local gui_elem = event.element + if (gui_elem.tags.action ~= "oarc_settings_tab") then return end + local index = gui_elem.tags.setting + + local entry = OCFG_KEYS[index] + if (entry.type == "boolean") then + settings.global[entry.mod_key] = { value = gui_elem.state } + end +end + +---Handles the text entry event for the tab used by AddOarcGuiTab +---@param event EventData.on_gui_text_changed +---@return nil +function SettingsControlsTabGuiTextChanged(event) + if not (event.element.valid) then return end + + local gui_elem = event.element + if (gui_elem.tags.action ~= "oarc_settings_tab") then return end + local index = gui_elem.tags.setting + local value = gui_elem.text + local entry = OCFG_KEYS[index] + + if (entry.type == "string") then + gui_elem.style = "invalid_value_textfield" + elseif (entry.type == "integer") then + gui_elem.style = "invalid_value_textfield" + gui_elem.style.width = 50 + end +end + +---Handles the confirmed text entry event +---@param event EventData.on_gui_confirmed +---@return nil +function SettingsControlsTabGuiTextconfirmed(event) + if not (event.element.valid) then return end + + local gui_elem = event.element + if (gui_elem.tags.action ~= "oarc_settings_tab") then return end + local index = gui_elem.tags.setting + local value = gui_elem.text + local entry = OCFG_KEYS[index] + + if (entry.type == "string") then + if value == "" then -- Force a non-empty string! + value = " " + gui_elem.text = " " + end + gui_elem.style = "textbox" + settings.global[entry.mod_key] = { value = gui_elem.text } + elseif (entry.type == "integer") then + local safe_value = GetSafeIntValueForModSetting(value, entry.mod_key) + if not pcall(function() settings.global[entry.mod_key] = { value = safe_value } end) then + settings.global[entry.mod_key] = { value = game.mod_setting_prototypes[entry.mod_key].default_value } + log("Error setting value for " .. entry.mod_key .. " to " .. safe_value) + end + gui_elem.text = tostring(settings.global[entry.mod_key].value) + gui_elem.style = "textbox" + gui_elem.style.width = 50 + + local slider = gui_elem.parent["slider"] + slider.slider_value = settings.global[entry.mod_key].value --[[@as integer]] + elseif (entry.type == "double") then + local safe_value = GetSafeDoubleValueForModSetting(value, entry.mod_key) + if not pcall(function() settings.global[entry.mod_key] = { value = safe_value } end) then + settings.global[entry.mod_key] = { value = game.mod_setting_prototypes[entry.mod_key].default_value } + log("Error setting value for " .. entry.mod_key .. " to " .. safe_value) + end + gui_elem.text = string.format("%.2f", settings.global[entry.mod_key].value) + gui_elem.style = "textbox" + gui_elem.style.width = 50 + + local slider = gui_elem.parent["slider"] + slider.slider_value = settings.global[entry.mod_key].value --[[@as number]] + end +end + + +---Handles slider value changes +---@param event EventData.on_gui_value_changed +---@return nil +function SettingsControlsTabGuiValueChanged(event) + if not (event.element.valid) then return end + + local gui_elem = event.element + if (gui_elem.tags.action ~= "oarc_settings_tab_slider") then return end + local index = gui_elem.tags.setting + local value = gui_elem.slider_value + local entry = OCFG_KEYS[index] + + if (entry.type == "integer") then + local textfield = gui_elem.parent["textfield"] + settings.global[entry.mod_key] = { value = value } -- Assumes that the slider can only produce valid inputs! + textfield.text = tostring(value) + elseif (entry.type == "double") then + local textfield = gui_elem.parent["textfield"] + settings.global[entry.mod_key] = { value = value } -- Assumes that the slider can only produce valid inputs! + textfield.text = string.format("%.2f", value) + end +end + +---Makes sure a given value is within the min/max range of a mod setting +---@param input string|number|integer +---@param mod_key string +---@return integer +function GetSafeIntValueForModSetting(input, mod_key) + local value_num = tonumber(input) + if not value_num then + value_num = tonumber(game.mod_setting_prototypes[mod_key].default_value) + else + local minimum = game.mod_setting_prototypes[mod_key].minimum_value + local maximum = game.mod_setting_prototypes[mod_key].maximum_value + if minimum ~= nil then + value_num = math.max(value_num, minimum) + end + if maximum ~= nil then + value_num = math.min(value_num, maximum) + end + value_num = math.floor(value_num) + end + return value_num --[[@as integer]] +end + +---Makes sure a given value is within the min/max range of a mod setting (double) +---@param input string|number|integer +---@param mod_key string +---@return number +function GetSafeDoubleValueForModSetting(input, mod_key) + local value_num = tonumber(input) + if not value_num then + value_num = tonumber(game.mod_setting_prototypes[mod_key].default_value) + else + local minimum = game.mod_setting_prototypes[mod_key].minimum_value + local maximum = game.mod_setting_prototypes[mod_key].maximum_value + if minimum ~= nil then + value_num = math.max(value_num, minimum) + end + if maximum ~= nil then + value_num = math.min(value_num, maximum) + end + end + return value_num --[[@as number]] +end + +---Creates a checkbox setting +---@param tab_container LuaGuiElement +---@param index string +---@param entry OarcSettingsLookup +---@param enabled boolean +---@return nil +function AddCheckboxSetting(tab_container, index, entry, enabled) + tab_container.add{ + type = "checkbox", + caption = { "mod-setting-name."..entry.mod_key }, + state = GetGlobalOarcConfigUsingKeyTable(entry.ocfg_keys), + enabled = enabled, + tooltip = { "mod-setting-description."..entry.mod_key }, + tags = { action = "oarc_settings_tab", setting = index }, + } +end + +---Creates a textfield setting +---@param tab_container LuaGuiElement +---@param index string +---@param entry OarcSettingsLookup +---@param enabled boolean +---@return nil +function AddTextfieldSetting(tab_container, index, entry, enabled) + local horizontal_flow = tab_container.add { + type = "flow", + direction = "horizontal", + } + horizontal_flow.add { + type = "label", + caption = { "mod-setting-name."..entry.mod_key }, + tooltip = { "mod-setting-description."..entry.mod_key }, + } + local dragger = horizontal_flow.add { + type = "empty-widget", + } + dragger.style.horizontally_stretchable = true + + local tooltip = {"", {"mod-setting-description."..entry.mod_key }, " ", { "oarc-settings-tab-text-field-enter-tooltip" }} + + horizontal_flow.add { + type = "textfield", + caption = { "mod-setting-name."..entry.mod_key }, + text = GetGlobalOarcConfigUsingKeyTable(entry.ocfg_keys), + enabled = enabled, + tooltip = tooltip, + tags = { action = "oarc_settings_tab", setting = index }, + } +end + +---Creates an integer setting +---@param tab_container LuaGuiElement +---@param index string +---@param entry OarcSettingsLookup +---@param enabled boolean +---@return nil +function AddIntegerSetting(tab_container, index, entry, enabled) + local horizontal_flow = tab_container.add { + type = "flow", + direction = "horizontal", + } + horizontal_flow.add { + type = "label", + caption = { "mod-setting-name."..entry.mod_key }, + tooltip = { "mod-setting-description."..entry.mod_key }, + } + local dragger = horizontal_flow.add { + type = "empty-widget", + } + dragger.style.horizontally_stretchable = true + + local slider = horizontal_flow.add { + name = "slider", + type = "slider", + minimum_value = game.mod_setting_prototypes[entry.mod_key].minimum_value, + maximum_value = game.mod_setting_prototypes[entry.mod_key].maximum_value, + value = GetGlobalOarcConfigUsingKeyTable(entry.ocfg_keys), + enabled = enabled, + tooltip = { "mod-setting-description."..entry.mod_key }, + tags = { action = "oarc_settings_tab_slider", setting = index }, + discrete_values = true, + value_step = 1, + } + + local tooltip = {"", {"mod-setting-description."..entry.mod_key }, " ", { "oarc-settings-tab-text-field-enter-tooltip" }} + local textfield = horizontal_flow.add { + name = "textfield", + type = "textfield", + numeric = true, + caption = { "mod-setting-name."..entry.mod_key }, + text = GetGlobalOarcConfigUsingKeyTable(entry.ocfg_keys), + enabled = enabled, + tooltip = tooltip, + tags = { action = "oarc_settings_tab", setting = index }, + } + textfield.style.width = 50 +end + +---Creates a double setting +---@param tab_container LuaGuiElement +---@param index string +---@param entry OarcSettingsLookup +---@param enabled boolean +---@return nil +function AddDoubleSetting(tab_container, index, entry, enabled) + local horizontal_flow = tab_container.add { + type = "flow", + direction = "horizontal", + } + horizontal_flow.add { + type = "label", + caption = { "mod-setting-name."..entry.mod_key }, + tooltip = { "mod-setting-description."..entry.mod_key }, + } + local dragger = horizontal_flow.add { + type = "empty-widget", + } + dragger.style.horizontally_stretchable = true + + local slider = horizontal_flow.add { + name = "slider", + type = "slider", + minimum_value = game.mod_setting_prototypes[entry.mod_key].minimum_value, + maximum_value = game.mod_setting_prototypes[entry.mod_key].maximum_value, + value = GetGlobalOarcConfigUsingKeyTable(entry.ocfg_keys), + enabled = enabled, + tooltip = { "mod-setting-description."..entry.mod_key }, + tags = { action = "oarc_settings_tab_slider", setting = index }, + discrete_values = false, + value_step = 0.01, + } + + local tooltip = {"", {"mod-setting-description."..entry.mod_key }, " ", { "oarc-settings-tab-text-field-enter-tooltip" }} + local textfield = horizontal_flow.add { + name = "textfield", + type = "textfield", + numeric = true, + allow_decimal = true, + caption = { "mod-setting-name."..entry.mod_key }, + text = string.format("%.2f", GetGlobalOarcConfigUsingKeyTable(entry.ocfg_keys)), + enabled = enabled, + tooltip = tooltip, + tags = { action = "oarc_settings_tab", setting = index }, + } + textfield.style.width = 50 +end + +---Create a dropdown setting for a string setting with allowed_values set +---@param tab_container LuaGuiElement +---@param index string +---@param entry OarcSettingsLookup +---@param enabled boolean +---@return nil +function AddStringListDropdownSetting(tab_container, index, entry, enabled) + local horizontal_flow = tab_container.add { + type = "flow", + direction = "horizontal", + } + horizontal_flow.add { + type = "label", + caption = { "mod-setting-name."..entry.mod_key }, + tooltip = { "mod-setting-description."..entry.mod_key }, + } + local dragger = horizontal_flow.add { + type = "empty-widget", + } + dragger.style.horizontally_stretchable = true + + local allowed_values = game.mod_setting_prototypes[entry.mod_key].allowed_values --[[@as string[] ]] + + local selected_index = 1 + for i,v in pairs(allowed_values) do + if (v == GetGlobalOarcConfigUsingKeyTable(entry.ocfg_keys)) then + selected_index = i + break + end + end + + local dropdown = horizontal_flow.add { + type = "drop-down", + items = allowed_values, + selected_index = selected_index, + enabled = enabled, + tooltip = { "mod-setting-description."..entry.mod_key }, + tags = { action = "oarc_settings_tab", setting = index }, + } +end + +---Creates a checkbox setting for surface related settings. +---@param parent LuaGuiElement +---@param surface_name string +---@param setting_name string +---@param state boolean +---@param admin boolean +---@param tooltip LocalisedString +---@return nil +function AddSurfaceCheckboxSetting(parent, surface_name, setting_name, state, admin, tooltip) + parent.add{ + name = surface_name.."_"..setting_name, + type = "checkbox", + state = state, + tags = { action = "oarc_settings_tab_surfaces", setting = setting_name, surface = surface_name }, + enabled = admin, + tooltip = tooltip, + } +end + +---Handles the click event for surface related settings +---@param event EventData.on_gui_click +---@return nil +function SettingsSurfaceControlsTabGuiClick(event) + if not (event.element.valid) then return end + + local gui_elem = event.element + if (gui_elem.tags.action ~= "oarc_settings_tab_surfaces") then return end + local setting_name = gui_elem.tags.setting + local surface_name = gui_elem.tags.surface --[[@as string]] + + if (setting_name == "spawn_enabled") then + global.oarc_surfaces[surface_name] = gui_elem.state + + if (#GetAllowedSurfaces() == 0) then + log("Warning - GetAllowedSurfaces() - No surfaces found! Forcing default surface!") + global.oarc_surfaces[global.ocfg.gameplay.default_surface] = true + event.element.parent[global.ocfg.gameplay.default_surface.."_spawn_enabled"].state = true + end + elseif (setting_name == "regrowth_enabled") then + + if (gui_elem.state) then + if not IsRegrowthEnabledOnSurface(surface_name) then + RegrowthEnableSurface(surface_name) + end + else + if IsRegrowthEnabledOnSurface(surface_name) then + RegrowthDisableSurface(surface_name) + end + end + end +end + + +---Handles dropdown selection events +---@param event EventData.on_gui_selection_state_changed +---@return nil +function SettingsControlsTabGuiSelectionStateChanged(event) + if not (event.element.valid) then return end + + local gui_elem = event.element + if (gui_elem.tags.action ~= "oarc_settings_tab") then return end + local index = gui_elem.tags.setting + local entry = OCFG_KEYS[index] + + if (entry.type == "string-list") then + settings.global[entry.mod_key] = { value = gui_elem.items[gui_elem.selected_index] } + end +end \ No newline at end of file diff --git a/lib/gui_tabs/spawn_controls.lua b/lib/gui_tabs/spawn_controls.lua new file mode 100644 index 0000000..2d034c1 --- /dev/null +++ b/lib/gui_tabs/spawn_controls.lua @@ -0,0 +1,377 @@ +-- Spawn control tab in the Oarc GUI, for things like sharing your base with others. + +---Provides the content of the spawn control tab in the Oarc GUI. +---@param tab_container LuaGuiElement +---@param player LuaPlayer +---@return nil +function CreateSpawnControlsTab(tab_container, player) + local spwnCtrls = tab_container.add { + type = "scroll-pane", + name = "spwn_ctrl_panel", + caption = "" + } + ApplyStyle(spwnCtrls, my_fixed_width_style) + spwnCtrls.style.maximal_height = 1000 + spwnCtrls.horizontal_scroll_policy = "never" + + CreatePrimarySpawnInfo(player, spwnCtrls) + CreateSecondarySpawnInfo(player, spwnCtrls) + CreateSetRespawnLocationButton(player, spwnCtrls) + + if global.ocfg.gameplay.enable_shared_spawns then + CreateSharedSpawnControls(player, spwnCtrls) + CreateJoinQueueControls(player, spwnCtrls) + end +end + +---Display some general info about the player's home base (different from respawn point). +---There should ONLY be one of these per player? Or maybe there are one per surface +---@param player LuaPlayer +---@param container LuaGuiElement +---@return nil +function CreatePrimarySpawnInfo(player, container) + local primary_spawn = FindPrimaryUniqueSpawn(player.name) + if (primary_spawn == nil) then return end + + AddLabel(container, nil, { "oarc-primary-spawn-info-header" }, "caption_label") + AddLabel(container, nil, { "oarc-primary-spawn-info-note" }, my_label_style) + + local horizontal_flow = container.add { type = "flow", direction = "horizontal"} + horizontal_flow.style.vertical_align = "center" + AddLabel(horizontal_flow, nil, { "oarc-primary-spawn-info-surface-label", + primary_spawn.surface_name, + primary_spawn.position.x, + primary_spawn.position.y}, my_label_style) + + --Add empty widget + local dragger = horizontal_flow.add { + type = "empty-widget", + style = "draggable_space_header" + } + dragger.style.horizontally_stretchable = true + + CreateGPSButton(horizontal_flow, primary_spawn.surface_name, primary_spawn.position) + + AddSpacerLine(container) +end + +---Display some general info about the player's secondary spawn points. +---@param player LuaPlayer +---@param container LuaGuiElement +---@return nil +function CreateSecondarySpawnInfo(player, container) + local secondary_spawns = FindSecondaryUniqueSpawns(player.name) + if (secondary_spawns == nil) or (table_size(secondary_spawns) == 0) then return end + + AddLabel(container, nil, { "oarc-secondary-spawn-info-header" }, "caption_label") + AddLabel(container, nil, { "oarc-secondary-spawn-info-note" }, my_label_style) + + for _,secondary_spawn in pairs(secondary_spawns) do + local horizontal_flow = container.add { type = "flow", direction = "horizontal"} + horizontal_flow.style.vertical_align = "center" + AddLabel(horizontal_flow, nil, { "oarc-secondary-spawn-info-surface-label", + secondary_spawn.surface_name, + secondary_spawn.position.x, + secondary_spawn.position.y}, my_label_style) + + --Add empty widget + local dragger = horizontal_flow.add { + type = "empty-widget", + style = "draggable_space_header" + } + dragger.style.horizontally_stretchable = true + + CreateGPSButton(horizontal_flow, secondary_spawn.surface_name, secondary_spawn.position) + end + + AddSpacerLine(container) +end + +---Display the shared spawn controls +---@param player LuaPlayer +---@param container LuaGuiElement +---@return nil +function CreateSharedSpawnControls(player, container) + local primary_spawn = FindPrimaryUniqueSpawn(player.name) + if (primary_spawn == nil) then return end + + AddLabel(container, nil, { "oarc-shared-spawn-controls" }, "caption_label") + + local shared_spawn_open = IsSharedSpawnOpen(primary_spawn.surface_name, player.name) + local shared_spawn_full = IsSharedSpawnFull(primary_spawn.surface_name, player.name) + + -- This checkbox allows people to join your base when they first start the game. + local toggle = container.add { + type = "checkbox", + name = "accessToggle", + tags = { action = "oarc_spawn_ctrl_tab", setting = "shared_access_toggle" }, + caption = { "oarc-shared-spawn-allow-joiners" }, + state = shared_spawn_open, + enabled = not shared_spawn_full -- Disable if the shared spawn is full + } + + if shared_spawn_open and shared_spawn_full then + AddLabel(container, nil, { "oarc-shared-spawn-full" }, my_note_style) + end + + ApplyStyle(toggle, my_fixed_width_style) +end + +---Display the set respawn location button in the spawn control tab. +---@param player LuaPlayer +---@param container LuaGuiElement +---@return nil +function CreateSetRespawnLocationButton(player, container) + AddLabel(container, nil, { "oarc-set-respawn-loc-header" }, "caption_label") + + --[[@type OarcPlayerSpawn]] + local respawn_info = global.player_respawns[player.name][player.surface.name] + + if (respawn_info == nil) then + log("ERROR: No respawn info for player: " .. player.name) + return + end + + local respawn_surface_name = respawn_info.surface + local respawn_position = respawn_info.position + + -- Display the current respawn location + local horizontal_flow = container.add { type = "flow", direction = "horizontal"} + horizontal_flow.style.vertical_align = "center" + AddLabel(horizontal_flow, nil, { "oarc-set-respawn-loc-info-surface-label", + respawn_surface_name, + respawn_position.x, + respawn_position.y }, my_label_style) + + --Add empty widget + local dragger = horizontal_flow.add { + type = "empty-widget", + style = "draggable_space_header" + } + dragger.style.horizontally_stretchable = true + + CreateGPSButton(horizontal_flow, respawn_surface_name, respawn_position) + + -- Sets the player's custom spawn point to their current location + if ((game.tick - global.player_cooldowns[player.name].setRespawn) > + (global.ocfg.gameplay.respawn_cooldown_min * TICKS_PER_MINUTE)) then + local change_respawn_button = container.add { + type = "button", + tags = { action = "oarc_spawn_ctrl_tab", setting = "set_respawn_location" }, + name = "setRespawnLocation", + caption = { "oarc-set-respawn-loc" }, + tooltip = { "oarc-set-respawn-loc-tooltip" }, + style = "red_button" + } + change_respawn_button.style.font = "default-small-semibold" + else + AddLabel(container, nil, + { "oarc-set-respawn-loc-cooldown", FormatTime((global.ocfg.gameplay.respawn_cooldown_min * TICKS_PER_MINUTE) - + (game.tick - global.player_cooldowns[player.name].setRespawn)) }, my_note_style) + end + AddLabel(container, nil, { "oarc-set-respawn-note" }, my_label_style) + AddSpacerLine(container) +end + +---Display a list of people in the join queue for a shared spawn. +---@param player LuaPlayer +---@param container LuaGuiElement +---@return nil +function CreateJoinQueueControls(player, container) + local primary_spawn = FindPrimaryUniqueSpawn(player.name) + if (primary_spawn == nil) then return end + + local shared_spawn_open = IsSharedSpawnOpen(primary_spawn.surface_name, player.name) + local shared_spawn_full = IsSharedSpawnFull(primary_spawn.surface_name, player.name) + + -- Only show this if the player has an open and not full shared spawn + if (not shared_spawn_open or shared_spawn_full) then return end + + if (table_size(primary_spawn.join_queue) > 0) then + AddLabel(container, nil, { "oarc-join-queue-header" }, "caption_label") + AddLabel(container, "drop_down_msg_lbl1", { "oarc-select-player-join-queue" }, my_label_style) + + local horizontal_flow = container.add { type = "flow", direction = "horizontal" } + horizontal_flow.style.horizontally_stretchable = true + + horizontal_flow.add { + name = "join_queue_dropdown", + type = "drop-down", + items = primary_spawn.join_queue + } + + local dragger = horizontal_flow.add { + type = "empty-widget", + style = "draggable_space_header" + } + dragger.style.horizontally_stretchable = true + + horizontal_flow.add { + name = "accept_player_request", + tags = { action = "oarc_spawn_ctrl_tab", setting = "accept_player_request" }, + type = "button", + style = "green_button", + caption = { "oarc-accept" } + } + horizontal_flow.add { + name = "reject_player_request", + tags = { action = "oarc_spawn_ctrl_tab", setting = "reject_player_request" }, + type = "button", + style = "red_button", + caption = { "oarc-reject" } + } + else + AddLabel(container, "empty_join_queue_note1", { "oarc-no-player-join-reqs" }, my_note_style) + end + +end + + +---Display a GPS button for a specific location. +---@param container LuaGuiElement +---@param surface_name string +---@param position MapPosition +---@return nil +function CreateGPSButton(container, surface_name, position) + local gps_button = container.add { + type = "sprite-button", + sprite = "utility/gps_map_icon", + tags = { + action = "oarc_spawn_ctrl_tab", + setting = "show_location", + surface = surface_name, + position = position + }, + style = "slot_button", + tooltip = { "oarc-spawn-info-location-button-tooltip" }, + } + gps_button.style.width = 28 + gps_button.style.height = 28 +end + +---Handle the gui checkboxes & radio buttons of the spawn control tab in the Oarc GUI. +---@param event EventData.on_gui_checked_state_changed +---@return nil +function SpawnCtrlGuiOptionsSelect(event) + if not event.element.valid then return end + local player = game.players[event.player_index] + local tags = event.element.tags + + if (tags.action ~= "oarc_spawn_ctrl_tab") then + return + end + + -- Handle changes to spawn sharing. + if (tags.setting == "shared_access_toggle") then + if event.element.state then + SendBroadcastMsg({ "oarc-start-shared-base", player.name }) + else + SendBroadcastMsg({ "oarc-stop-shared-base", player.name }) + end + local primary_spawn = FindPrimaryUniqueSpawn(player.name) + global.unique_spawns[primary_spawn.surface_name][player.name].open_access = event.element.state + OarcGuiRefreshContent(player) + + -- Refresh the shared spawn spawn gui for all players + for _,p in pairs(game.connected_players) do + RefreshSharedSpawnFrameIfExist(p) + end + end +end + +---Handle the gui click of the spawn control tab in the Oarc GUI. +---@param event EventData.on_gui_click +---@return nil +function SpawnCtrlGuiClick(event) + if not event.element.valid then return end + local player = game.players[event.player_index] + local tags = event.element.tags + + if (tags.action ~= "oarc_spawn_ctrl_tab") then + return + end + + -- Sets a new respawn point and resets the cooldown. + if (tags.setting == "set_respawn_location") then + SetPlayerRespawn(player.name, player.surface.name, player.position, true) + OarcGuiRefreshContent(player) + player.print({ "oarc-spawn-point-updated" }) + + -- Shows the spawn location on the map + elseif (tags.setting == "show_location") then + local surface_name = tags.surface --[[@as string]] + local position = tags.position --[[@as MapPosition]] + player.open_map(position, 0.05) + player.print({"", { "oarc-spawn-gps-location" }, GetGPStext(surface_name, position)}) + + -- Accept or reject pending player join requests to a shared base + elseif ((tags.setting == "accept_player_request") or (tags.setting == "reject_player_request")) then + + if ((event.element.parent.join_queue_dropdown == nil) or + (event.element.parent.join_queue_dropdown.selected_index == 0)) then + player.print({ "oarc-selected-player-not-valid" }) + OarcGuiRefreshContent(player) + return + end + + local join_queue_index = event.element.parent.join_queue_dropdown.selected_index + local join_queue_player_choice = event.element.parent.join_queue_dropdown.get_item(join_queue_index) --[[@as string]] + + -- Shouldn't be able to hit this since we force a GUI refresh when they leave? + if ((game.players[join_queue_player_choice] == nil) or (not game.players[join_queue_player_choice].connected)) then + player.print({ "oarc-selected-player-not-wait" }) + OarcGuiRefreshContent(player) + return + end + + local primary_spawn = FindPrimaryUniqueSpawn(player.name) + + if (tags.setting == "reject_player_request") then + + RemovePlayerFromJoinQueue(join_queue_player_choice) -- This also refreshes the host gui + + -- Inform the host that the player was rejected + player.print({ "oarc-reject-joiner", join_queue_player_choice }) + -- Inform the player that their request was rejected + SendMsg(join_queue_player_choice, { "oarc-your-request-rejected" }) + + -- Close the waiting players menu + if (game.players[join_queue_player_choice].gui.screen.join_shared_spawn_wait_menu) then + game.players[join_queue_player_choice].gui.screen.join_shared_spawn_wait_menu.destroy() + DisplaySpawnOptions(game.players[join_queue_player_choice]) + end + + elseif (tags.setting == "accept_player_request") then + + -- Check if there is space first + if (table_size(primary_spawn.joiners) >= global.ocfg.gameplay.number_of_players_per_shared_spawn - 1) then + player.print({ "oarc-shared-spawn-full" }) + return + end + + -- Send an announcement + SendBroadcastMsg({ "oarc-player-joining-base", join_queue_player_choice, player.name }) + + -- Close the waiting players menu + if (game.players[join_queue_player_choice].gui.screen.join_shared_spawn_wait_menu) then + game.players[join_queue_player_choice].gui.screen.join_shared_spawn_wait_menu.destroy() + end + + -- Spawn the player + local joining_player = game.players[join_queue_player_choice] + SetPlayerRespawn(joining_player.name, primary_spawn.surface_name, primary_spawn.position, true) + SendPlayerToSpawn(primary_spawn.surface_name, joining_player) + GivePlayerStarterItems(joining_player) + table.insert(global.unique_spawns[primary_spawn.surface_name][player.name].joiners, joining_player.name) + joining_player.force = game.players[player.name].force + + -- Render some welcoming text... + DisplayWelcomeGroundTextAtSpawn(joining_player, primary_spawn.surface_name, primary_spawn.position) + + -- Unlock spawn control gui tab + SetOarcGuiTabEnabled(joining_player, OARC_SPAWN_CTRL_TAB_NAME, true) + + RemovePlayerFromJoinQueue(join_queue_player_choice) -- This also refreshes the host gui + end + end +end diff --git a/lib/helper_commands.lua b/lib/helper_commands.lua deleted file mode 100644 index fa3e8f0..0000000 --- a/lib/helper_commands.lua +++ /dev/null @@ -1,67 +0,0 @@ --- helper_commands.lua --- Jan 2018 --- None of this is my code. - -require("lib/oarc_utils") - -commands.add_command("run", "change player speed bonus", function(command) - local player = game.players[command.player_index]; - if player ~= nil then - if (command.parameter ~= nil) then - if command.parameter == "fast" then - player.character_running_speed_modifier = 1 - elseif command.parameter == "slow" then - player.character_running_speed_modifier = 0 - else - player.print("run fast | slow"); - end - end - end -end) - -commands.add_command("handcraft", "change player speed bonus", function(command) - local player = game.players[command.player_index]; - if player ~= nil then - if (command.parameter ~= nil) then - if command.parameter == "fast" then - player.character_crafting_speed_modifier = 5 - elseif command.parameter == "slow" then - player.character_crafting_speed_modifier = 0 - else - player.print("handcraft fast | slow"); - end - end - end -end) - -commands.add_command("mine", "change player speed bonus", function(command) - local player = game.players[command.player_index]; - if player ~= nil then - if (command.parameter ~= nil) then - if command.parameter == "fast" then - player.character_mining_speed_modifier = 2 - elseif command.parameter == "slow" then - player.character_mining_speed_modifier = 0 - else - player.print("mine fast | slow"); - end - end - end -end) - -commands.add_command("kit", "give a start kit", function(command) - local player = game.players[command.player_index]; - if player ~= nil and player.admin then - local target = player - if (command.parameter ~= nil) then - target = game.players[command.parameter] - end - if target ~= nil then - GivePlayerStarterItems(target); - player.print("gave a kit to " .. target.name); - target.print("you have been given a start kit"); - else - player.print("no player " .. command.parameter); - end - end -end) \ No newline at end of file diff --git a/lib/holding_pen.lua b/lib/holding_pen.lua new file mode 100644 index 0000000..47d16cd --- /dev/null +++ b/lib/holding_pen.lua @@ -0,0 +1,143 @@ +-- This file is used to create the holding pen area where players spawn in before being teleported to their own area. + +HOLDING_PEN_SURFACE_NAME = "oarc_holding_pen" + +function CreateHoldingPenSurface() + + if game.surfaces[HOLDING_PEN_SURFACE_NAME] ~= nil then + log("ERROR - Holding pen surface already exists!") + return + end + + ---@type MapGenSettings + ---@diagnostic disable-next-line: missing-fields + local map_settings = {} + map_settings.terrain_segmentation = "none" + map_settings.water = "none" + map_settings.starting_area = "none" + map_settings.peaceful_mode = true + map_settings.width = 64 + map_settings.height = 64 + + -- Create a new surface for the holding pen + + local holding_pen_surface = game.create_surface(HOLDING_PEN_SURFACE_NAME, map_settings) + holding_pen_surface.always_day = true + holding_pen_surface.show_clouds = false + holding_pen_surface.generate_with_lab_tiles = true + + RenderPermanentGroundText(holding_pen_surface, {x=9,y=-7}, 5, "O", {0.9, 0.7, 0.3, 0.5}, "center") + RenderPermanentGroundText(holding_pen_surface, {x=9,y=-4}, 5, "A", {0.9, 0.7, 0.3, 0.5}, "center") + RenderPermanentGroundText(holding_pen_surface, {x=9,y=-1}, 5, "R", {0.9, 0.7, 0.3, 0.5}, "center") + RenderPermanentGroundText(holding_pen_surface, {x=9,y=2}, 5, "C", {0.9, 0.7, 0.3, 0.5}, "center") +end + +---Creates a holding pen area +---@param event EventData.on_chunk_generated +---@return nil +function CreateHoldingPenChunks(event) + + local surface = event.surface + local chunk_area = event.area + local chunk_position = event.position + + + if (surface.name ~= HOLDING_PEN_SURFACE_NAME) then + return + end + + -- Remove ALL entities in the chunk + for _, entity in pairs(surface.find_entities(chunk_area)) do + if entity.type ~= "character" then + entity.destroy() + end + end + + -- Place tiles and trees and water for the holding pen + local tiles = {} + for x=chunk_area.left_top.x,chunk_area.right_bottom.x,1 do + for y=chunk_area.left_top.y,chunk_area.right_bottom.y,1 do + local distance_sqr = math.floor(x^2 + y^2) + + if (distance_sqr < 15^2) then + table.insert(tiles, {name="grass-1", position={x, y}}) + elseif (distance_sqr < 20^2) then + table.insert(tiles, {name="water", position={x, y}}) + + --10% chance of fish in water + if (math.random(1,10) == 1) then + surface.create_entity({name="fish", position={x + 0.5, y + 0.5}}) + end + + else + table.insert(tiles, {name="out-of-map", position={x, y}}) + end + + if (distance_sqr >= 13^2) and (distance_sqr <= 15^2) then + surface.create_entity({name="tree-01", position={x + 0.5, y + 0.5}}) + end + end + end + surface.set_tiles(tiles) + + -- If this is the bottom right chunk it's safe to place stuff inside the holding pen now. + if (chunk_position.x == 2 and chunk_position.y == 2) then + + PlaceResourcesInSemiCircleHoldingPen(surface, {x=0,y=0}, 0.2, 0.1) + + CreateWaterStrip(surface, {x=-2,y=-11}, 4) + CreateWaterStrip(surface, {x=-2,y=-10}, 4) + + surface.create_entity({ + name = "crude-oil", + amount = 90000, + position = { 0, 9 } + }) + end +end + + + +---A special version of PlaceResourcesInSemiCircle for the holding pen +---@param surface LuaSurface +---@param position TilePosition --The center of the spawn area +---@param size_mod number +---@param amount_mod number +---@return nil +function PlaceResourcesInSemiCircleHoldingPen(surface, position, size_mod, amount_mod) + + local resources = global.ocfg.surfaces_config["nauvis"].spawn_config.solid_resources + + -- Create list of resource tiles + ---@type table + local r_list = {} + for r_name, _ in pairs(resources) do + if (r_name ~= "") then + table.insert(r_list, r_name) + end + end + ---@type table + local shuffled_list = FYShuffle(r_list) + + -- This places resources in a semi-circle + local angle_offset = 2.32 + local num_resources = table_size(resources) + local theta = ((4.46 - 2.32) / num_resources); + local count = 0 + + -- Unique to the holding pen size + local radius = 15 - 6 + + for _, r_name in pairs(shuffled_list) do + local angle = (theta * count) + angle_offset; + + 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 = resources[r_name] + GenerateResourcePatch(surface, r_name, resourceConfig.size * size_mod, pos, resourceConfig.amount * amount_mod) + count = count + 1 + end +end \ No newline at end of file diff --git a/lib/map_features.lua b/lib/map_features.lua deleted file mode 100644 index 230ad50..0000000 --- a/lib/map_features.lua +++ /dev/null @@ -1,970 +0,0 @@ --- map_features.lua --- April 2020 --- Oarc's clone of whistlestop factories maybe? - --- Generic Utility Includes -require("lib/oarc_utils") - - --- Used to generate placement of buildings. -MAGIC_BUILDING_MIN_DISTANCE = 40 -MAGIC_BUILDING_MAX_DISTANCE = FAR_MAX_DIST + 50 -MAGIC_BUILDING_CHUNK_SPREAD = 41 - - -POWER_USAGE_SCALING_FACTOR = 2 - --- This is a table indexed by the single INPUT item! -FURNACE_ENERGY_PER_CRAFT_SECOND = (180000 / 2) * POWER_USAGE_SCALING_FACTOR -FURNACE_RECIPES = { - ["iron-ore"] = {recipe_name = "iron-plate", - recipe_energy = 3.2*FURNACE_ENERGY_PER_CRAFT_SECOND, - recipe_pollution = 0.053}, - ["copper-ore"] = {recipe_name = "copper-plate", - recipe_energy = 3.2*FURNACE_ENERGY_PER_CRAFT_SECOND, - recipe_pollution = 0.053}, - ["iron-plate"] = {recipe_name = "steel-plate", - recipe_energy = 16*FURNACE_ENERGY_PER_CRAFT_SECOND, - recipe_pollution = 0.267}, - ["stone"] = {recipe_name = "stone-brick", - recipe_energy = 3.2*FURNACE_ENERGY_PER_CRAFT_SECOND, - recipe_pollution = 0.053}, -} - --- The chemplants/refineries/assemblers lookup their own recipes since they can be set by the player. -CHEMPLANT_ENERGY_PER_CRAFT_SECOND = 210000 * POWER_USAGE_SCALING_FACTOR -REFINERY_ENERGY_PER_CRAFT_SECOND = 420000 * POWER_USAGE_SCALING_FACTOR -ASSEMBLER3_ENERGY_PER_CRAFT_SECOND = (375000 / 1.25) * POWER_USAGE_SCALING_FACTOR -CENTRIFUGE_ENERGY_PER_CRAFT_SECOND = 350000 * POWER_USAGE_SCALING_FACTOR - -CHEMPLANT_POLLUTION_PER_CRAFT_SECOND = 4/60 -REFINERY_POLLUTION_PER_CRAFT_SECOND = 6/60 -ASSEMBLER3_POLLUTION_PER_CRAFT_SECOND = 2/60 -CENTRIFUGE_POLLUTION_PER_CRAFT_SECOND = 4/60 - - -ENEMY_WORM_TURRETS = -{ - [0] = "small-worm-turret", - [1] = "medium-worm-turret", - [2] = "big-worm-turret" -} - -NEUTRAL_FORCE_RECIPES = -{ - -- Science packs - ["automation-science-pack"] = true, - ["chemical-science-pack"] = true, - ["logistic-science-pack"] = true, - ["military-science-pack"] = true, - ["production-science-pack"] = true, - ["utility-science-pack"] = true, - - -- Oil Stuff - ["advanced-oil-processing"] = true, - ["basic-oil-processing"] = true, - -- ["coal-liquefaction"] = true, -- Too difficult/costly to implement - - ["heavy-oil-cracking"] = true, - ["light-oil-cracking"] = true, - - ["solid-fuel-from-heavy-oil"] = true, - ["solid-fuel-from-light-oil"] = true, - ["solid-fuel-from-petroleum-gas"] = true, - - ["lubricant"] = true, - ["plastic-bar"] = true, - ["sulfur"] = true, - ["sulfuric-acid"] = true, - - -- ["oil-refinery"] = true, - -- ["explosives"] = true, - - -- Modules - ["effectivity-module"] = true, - ["effectivity-module-2"] = true, - ["effectivity-module-3"] = true, - ["productivity-module"] = true, - ["productivity-module-2"] = true, - ["productivity-module-3"] = true, - ["speed-module"] = true, - ["speed-module-2"] = true, - ["speed-module-3"] = true, - - -- Intermediates - ["advanced-circuit"] = true, - ["battery"] = true, - ["copper-cable"] = true, - ["copper-plate"] = true, - ["electric-engine-unit"] = true, - ["electronic-circuit"] = true, - ["engine-unit"] = true, - ["flying-robot-frame"] = true, - ["iron-gear-wheel"] = true, - ["iron-plate"] = true, - ["iron-stick"] = true, - ["low-density-structure"] = true, - ["processing-unit"] = true, - ["rocket-control-unit"] = true, - ["rocket-fuel"] = true, - ["steel-plate"] = true, - ["stone-brick"] = true, - - -- Misc - ["concrete"] = true, - ["landfill"] = true, - ["rail"] = true, - ["solar-panel"] = true, - ["stone-wall"] = true, - ["empty-barrel"] = true, - - -- Nuclear - ["uranium-processing"] = true, - -- ["kovarex-enrichment-process"] = true, - -- ["nuclear-fuel-reprocessing"] = true, - - -- ["pipe"] = true, - -- ["pipe-to-ground"] = true, -} - -function SetNeutralForceAllowedRecipes() - - -- Neutral force requires recipes so that furnaces can smelt steel for example. - -- game.forces["neutral"].enable_all_recipes() - - -- Disable ALL recipes - for i,v in pairs(game.forces["neutral"].recipes) do - game.forces["neutral"].recipes[i].enabled = false; - end - - -- Enable only the ones we want - for i,v in pairs(NEUTRAL_FORCE_RECIPES) do - game.forces["neutral"].recipes[i].enabled = true; - end - -end - -function MagicFactoriesInit() - - SetNeutralForceAllowedRecipes() - - global.omagic = {} - global.omagic.building_total_count = 0 - global.omagic.factory_positions = {} - global.omagic.furnaces = {} - global.omagic.chemplants = {} - global.omagic.refineries = {} - global.omagic.assemblers = {} - global.omagic.centrifuges = {} - - MagicFactoryChunkGenerator() - - game.surfaces[GAME_SURFACE_NAME].force_generate_chunk_requests() -- Block and generate all to be sure. - MagicalFactorySpawnAll() -end - -function MagicFactoryChunkGenerator() - - -- This generates several circles of randomized chunk positions. - for r=MAGIC_BUILDING_MIN_DISTANCE,MAGIC_BUILDING_MAX_DISTANCE,MAGIC_BUILDING_CHUNK_SPREAD do - local random_angle_offset = math.random(0, math.pi * 2) - local num_positions_for_circle = math.ceil((r/8)) -- This makes it so each circle has more dots, roughly spreads things out equally. - - for i=1,num_positions_for_circle do - local theta = ((math.pi * 2) / num_positions_for_circle); - local angle = (theta * i) + random_angle_offset; - - local chunk_x = MathRound((r * math.cos(angle)) + math.random(-2, 2)) - local chunk_y = MathRound((r * math.sin(angle)) + math.random(-2, 2)) - - if (not game.surfaces[GAME_SURFACE_NAME].is_chunk_generated({chunk_x,chunk_y})) then - - table.insert(global.omagic.factory_positions, {x=chunk_x, y=chunk_y}) - game.surfaces[GAME_SURFACE_NAME].request_to_generate_chunks(GetCenterTilePosFromChunkPos({x=chunk_x, y=chunk_y}), 0) - log("Magic furnace position: " .. chunk_x .. ", " .. chunk_y .. ", " .. angle) - else - log("Magic furnace collided with silo location?" .. chunk_x .. ", " .. chunk_y) - end - end - end - - SendBroadcastMsg("Number magic chunks: " .. #global.omagic.factory_positions) -end - -function FindClosestMagicChunk(player) - if (not player or not player.character) then return end - return GetClosestPosFromTable(GetChunkPosFromTilePos(player.character.position), global.omagic.factory_positions) -end - -function IndicateClosestMagicChunk(player) - local target_pos = GetCenterTilePosFromChunkPos(FindClosestMagicChunk(player)) - rendering.draw_line{color={r=0.5,g=0.5,b=0.5,a=0.5}, - width=2, - from=player.character, - to=target_pos, - surface=player.character.surface, - players={player}, - draw_on_ground=true, - time_to_live=60*5} -end - -function MagicalFactorySpawnAll() - for _,chunk_pos in pairs(global.omagic.factory_positions) do - - local pos = GetCenterTilePosFromChunkPos(chunk_pos) - local c_area = GetAreaFromChunkPos(chunk_pos) - - -- Remove any entities in the chunk area. - for _, entity in pairs(game.surfaces[GAME_SURFACE_NAME].find_entities_filtered{area=c_area}) do - entity.destroy() - end - - -- Place landfill underneath - local dirtTiles = {} - for i=c_area.left_top.x,c_area.right_bottom.x,1 do - for j=c_area.left_top.y,c_area.right_bottom.y,1 do - table.insert(dirtTiles, {name = "landfill", position ={i,j}}) - end - end - game.surfaces[GAME_SURFACE_NAME].set_tiles(dirtTiles) - - -- Yay colored tiles - CreateFixedColorTileArea(game.surfaces[GAME_SURFACE_NAME], - {left_top = {x=c_area.left_top.x+2, y=c_area.left_top.y+2}, - right_bottom = {x=c_area.right_bottom.x-2, y=c_area.right_bottom.y-2}}, - "black") - - -- Make it safe from regrowth - if global.ocfg.enable_regrowth then - RegrowthMarkAreaSafeGivenTilePos(pos, 0, true) - end - end -end - -function SpawnEnemyTurret(pos) - - local turret = game.surfaces[GAME_SURFACE_NAME].create_entity{name="gun-turret", position=pos, force="enemy"} - local turret_inv = turret.get_inventory(defines.inventory.turret_ammo) - turret_inv.insert({name="uranium-rounds-magazine", count=200}) - -end - -function RequestSpawnSpecialChunk(player, spawn_function, feature_name) - local closest_chunk = FindClosestMagicChunk(player) - local player_chunk = GetChunkPosFromTilePos(player.character.position) - if ((closest_chunk.x == player_chunk.x) and (closest_chunk.y == player_chunk.y)) then - local chunk_area = GetAreaFromChunkPos(closest_chunk) - - local entities = game.surfaces[GAME_SURFACE_NAME].find_entities_filtered{ - area={left_top = {chunk_area.left_top.x+1, chunk_area.left_top.y+1}, - right_bottom = {chunk_area.right_bottom.x-1, chunk_area.right_bottom.y-1}}, - force={"enemy"}, - invert=true} - - -- Either there are no entities in the chunk (player is just on the boundary), or the only entity is the player. - if ((#entities == 1) and (entities[1].player) and (entities[1].player == player)) or (#entities == 0) then - spawn_function(closest_chunk) - -- Teleport to center of chunk to be safe. - SafeTeleport(player, game.surfaces[GAME_SURFACE_NAME], GetCenterTilePosFromChunkPos(closest_chunk)) - OarcMapFeaturePlayerCountChange(player, "special_chunks", feature_name, 1) - return true - else - player.print("Looks like this chunk already has something in it other than just you the player?! " .. entities[1].name) - return false - end - - else - player.print("You need to be standing inside the special chunk!") - return false - end - - return false -end - -function SpecialChunkHelperText(pos) - RenderPermanentGroundText(game.surfaces[GAME_SURFACE_NAME].index, - {x=pos.x-3.5,y=pos.y+1}, - 1, - "Supply energy to this interface!", - {0.7,0.4,0.3,0.8}) - RenderPermanentGroundText(game.surfaces[GAME_SURFACE_NAME].index, - {x=pos.x-4.5,y=pos.y+2}, - 1, - "Modules/beacons DO NOT have any effect!", - {0.7,0.4,0.3,0.8}) -end - -function spawnSpecialChunkInputElec(center_pos) - local inputElec = game.surfaces[GAME_SURFACE_NAME].create_entity{name="electric-energy-interface", position=center_pos, force="neutral"} - inputElec.destructible = false - inputElec.minable = false - inputElec.operable = false - inputElec.electric_buffer_size = 1000000000 - inputElec.power_production = 0 - inputElec.power_usage = 0 - inputElec.energy = 0 - return inputElec -end - -function SpawnFurnaceChunk(chunk_pos) - - center_pos = GetCenterTilePosFromChunkPos(chunk_pos) - local furnace_chunk = {["energy_input"] = spawnSpecialChunkInputElec(center_pos), - ["entities"] = {}} - - -- 4x furnaces - table.insert(furnace_chunk.entities, SpawnMagicBuilding("electric-furnace", {x=center_pos.x-12,y=center_pos.y-12})) - table.insert(furnace_chunk.entities, SpawnMagicBuilding("electric-furnace", {x=center_pos.x+11,y=center_pos.y-12})) - table.insert(furnace_chunk.entities, SpawnMagicBuilding("electric-furnace", {x=center_pos.x-12,y=center_pos.y+11})) - table.insert(furnace_chunk.entities, SpawnMagicBuilding("electric-furnace", {x=center_pos.x+11,y=center_pos.y+11})) - - table.insert(global.omagic.furnaces, furnace_chunk) - SpecialChunkHelperText(center_pos) -end - -function SpawnOilRefineryChunk(chunk_pos) - - center_pos = GetCenterTilePosFromChunkPos(chunk_pos) - - local oil_chunk = {["energy_input"] = spawnSpecialChunkInputElec(center_pos), - ["chemplants"] = {}, - ["refineries"] = {}} - - -- 2x Refineries - table.insert(oil_chunk.refineries, SpawnMagicBuilding("oil-refinery", {x=center_pos.x-5,y=center_pos.y-8})) - table.insert(oil_chunk.refineries, SpawnMagicBuilding("oil-refinery", {x=center_pos.x+5,y=center_pos.y-8})) - - -- 6x Chem Plants - table.insert(oil_chunk.chemplants, SpawnMagicBuilding("chemical-plant", {x=center_pos.x-10,y=center_pos.y+8})) - table.insert(oil_chunk.chemplants, SpawnMagicBuilding("chemical-plant", {x=center_pos.x-6,y=center_pos.y+8})) - table.insert(oil_chunk.chemplants, SpawnMagicBuilding("chemical-plant", {x=center_pos.x-2,y=center_pos.y+8})) - table.insert(oil_chunk.chemplants, SpawnMagicBuilding("chemical-plant", {x=center_pos.x+2,y=center_pos.y+8})) - table.insert(oil_chunk.chemplants, SpawnMagicBuilding("chemical-plant", {x=center_pos.x+6,y=center_pos.y+8})) - table.insert(oil_chunk.chemplants, SpawnMagicBuilding("chemical-plant", {x=center_pos.x+10,y=center_pos.y+8})) - - table.insert(global.omagic.refineries, oil_chunk) - table.insert(global.omagic.chemplants, oil_chunk) - SpecialChunkHelperText(center_pos) -end - -function SpawnAssemblyChunk(chunk_pos) - - center_pos = GetCenterTilePosFromChunkPos(chunk_pos) - local assembler_chunk = {["energy_input"] = spawnSpecialChunkInputElec(center_pos), - ["entities"] = {}} - - -- 6x Assemblers - table.insert(assembler_chunk.entities, SpawnMagicBuilding("assembling-machine-3", {x=center_pos.x-12,y=center_pos.y-12})) - table.insert(assembler_chunk.entities, SpawnMagicBuilding("assembling-machine-3", {x=center_pos.x,y=center_pos.y-12})) - table.insert(assembler_chunk.entities, SpawnMagicBuilding("assembling-machine-3", {x=center_pos.x+11,y=center_pos.y-12})) - table.insert(assembler_chunk.entities, SpawnMagicBuilding("assembling-machine-3", {x=center_pos.x-12,y=center_pos.y+11})) - table.insert(assembler_chunk.entities, SpawnMagicBuilding("assembling-machine-3", {x=center_pos.x-1,y=center_pos.y+11})) - table.insert(assembler_chunk.entities, SpawnMagicBuilding("assembling-machine-3", {x=center_pos.x+11,y=center_pos.y+11})) - - table.insert(global.omagic.assemblers, assembler_chunk) - SpecialChunkHelperText(center_pos) -end - -function SpawnCentrifugeChunk(chunk_pos) - - center_pos = GetCenterTilePosFromChunkPos(chunk_pos) - local centrifuge_chunk = {["energy_input"] = spawnSpecialChunkInputElec(center_pos), - ["entities"] = {}} - - -- 1 Centrifuge (MORE THAN ENOUGH!) - table.insert(centrifuge_chunk.entities, SpawnMagicBuilding("centrifuge", {x=center_pos.x,y=center_pos.y-10})) - - table.insert(global.omagic.centrifuges, centrifuge_chunk) - SpecialChunkHelperText(center_pos) -end - -function SpawnSiloChunk(chunk_pos) - - center_pos = GetCenterTilePosFromChunkPos(chunk_pos) - - table.insert(global.siloPosition, center_pos) - - RenderPermanentGroundText(game.surfaces[GAME_SURFACE_NAME].index, - {x=center_pos.x-3.25,y=center_pos.y+6}, - 1, - "You can build a silo here!", - {0.7,0.4,0.3,0.8}) - - -- Set tiles below the silo - tiles = {} - for dx = -6,5 do - for dy = -6,5 do - if (game.active_mods["oarc-restricted-build"]) then - table.insert(tiles, {name = global.ocfg.locked_build_area_tile, - position = {center_pos.x+dx, center_pos.y+dy}}) - else - if ((dx % 2 == 0) or (dx % 2 == 0)) then - table.insert(tiles, {name = "concrete", - position = {center_pos.x+dx, center_pos.y+dy}}) - else - table.insert(tiles, {name = "hazard-concrete-left", - position = {center_pos.x+dx, center_pos.y+dy}}) - end - end - end - end - game.surfaces[GAME_SURFACE_NAME].set_tiles(tiles, true) -end - -function SpawnMagicBuilding(entity_name, position) - local direction = defines.direction.north - if (entity_name == "oil-refinery") then - direction = defines.direction.south - end - local magic_building = game.surfaces[GAME_SURFACE_NAME].create_entity{name=entity_name, position=position, force="neutral", direction=direction} - magic_building.destructible = false - magic_building.minable = false - magic_building.operable = true - magic_building.active = false - - global.omagic.building_total_count = global.omagic.building_total_count + 1 - - return magic_building -end - -function MagicFactoriesOnTick() - MagicFurnaceOnTick() - MagicChemplantOnTick() - MagicRefineryOnTick() - MagicAssemblerOnTick() - MagicCentrifugeOnTick() -end - --- Some helpful math: --- 94 per tick (max stack of ore in a smelter) (More like 2 or 3 ore per tick.) --- blue belt = 45 / sec --- 6 INPUT blue belts = 4.5 ore/tick (45 * 6 / 60) with productivity is an extra 0.9 maybe. -function MagicFurnaceOnTick() - if not global.omagic.furnaces then return end - - for entry_idx,entry in pairs(global.omagic.furnaces) do - - -- Validate the entry. - if (entry == nil) or (entry.entities == nil) or (entry.energy_input == nil) or (not entry.energy_input.valid) then - global.omagic.furnaces[entry_idx] = nil - log("MagicFurnaceOnTick - Magic furnace entry removed?") - goto next_furnace_entry - end - - local energy_share = entry.energy_input.energy/#entry.entities - - for idx,furnace in pairs(entry.entities) do - - if (furnace == nil) or (not furnace.valid) then - global.omagic.furnaces[entry_idx] = nil - log("MagicFurnaceOnTick - Magic furnace removed?") - goto next_furnace_entry - end - - local input_inv = furnace.get_inventory(defines.inventory.furnace_source) - local input_items = input_inv.get_contents() - - -- We have something inside? - local input_item_name = next(input_items) - if not input_item_name then - goto next_furnace - end - - -- Does the input item have a recipe? - if not FURNACE_RECIPES[input_item_name] then - log("MagicFurnaceOnTick - Missing FURNACE_RECIPES?") - goto next_furnace - end - local recipe = game.forces["neutral"].recipes[FURNACE_RECIPES[input_item_name].recipe_name] - if not recipe then - log("MagicFurnaceOnTick - Missing neutral force recipes?") - goto next_furnace - end - - -- Verify 1 ingredient type and 1 product type (for furnaces) - if (#recipe.products ~= 1) or (#recipe.ingredients ~= 1) then - log("MagicFurnaceOnTick - Recipe product/ingredient more than 1?") - goto next_furnace - end - local recipe_ingredient = recipe.ingredients[next(recipe.ingredients)] - local recipe_product = recipe.products[next(recipe.products)] - - local output_inv = furnace.get_inventory(defines.inventory.furnace_result) - - -- Can we insert at least 1 of the recipe result? - -- if not output_inv.can_insert({name=recipe_product.name}) then goto next_furnace end - local output_space = output_inv.get_insertable_count(recipe_product.name) - - -- Calculate how many times we can make the recipe. - local ingredient_limit = math.floor(input_items[input_item_name]/recipe_ingredient.amount) - local output_limit = math.floor(output_space/recipe_product.amount) - - -- Use shared energy pool - local energy_limit = math.floor(energy_share/FURNACE_RECIPES[input_item_name].recipe_energy) - local recipe_count = math.min(ingredient_limit, output_limit, energy_limit) - - -- Hit a limit somewhere? - if (recipe_count <= 0) then goto next_furnace end - - -- Track energy usage - entry.energy_input.energy = entry.energy_input.energy - (FURNACE_RECIPES[input_item_name].recipe_energy*recipe_count) - furnace.surface.pollute(furnace.position, FURNACE_RECIPES[input_item_name].recipe_pollution*recipe_count) - - -- Check if it has a last_user - if (not furnace.last_user) then - local player_entities = game.surfaces[GAME_SURFACE_NAME].find_entities_filtered{ - position=furnace.position, - radius=10, - force={"enemy", "neutral"}, - limit=1, - invert=true} - if (player_entities and player_entities[1] and player_entities[1].last_user) then - furnace.last_user = player_entities[1].last_user - end - end - - -- Subtract recipe count from input and Add recipe count to output - input_inv.remove({name=recipe_ingredient.name, count=recipe_count*recipe_ingredient.amount}) - output_inv.insert({name=recipe_product.name, count=recipe_count*recipe_product.amount}) - furnace.products_finished = furnace.products_finished + recipe_count - - -- If we have a user, do the stats - if (furnace.last_user) then - furnace.last_user.force.item_production_statistics.on_flow(recipe_ingredient.name, -recipe_count*recipe_ingredient.amount) - furnace.last_user.force.item_production_statistics.on_flow(recipe_product.name, recipe_count*recipe_product.amount) - end - - ::next_furnace:: - end - - ::next_furnace_entry:: - end -end - -function MagicChemplantOnTick() - if not global.omagic.chemplants then return end - - for entry_idx,entry in pairs(global.omagic.chemplants) do - - -- Validate the entry. - if (entry == nil) or (entry.chemplants == nil) or (entry.energy_input == nil) or (not entry.energy_input.valid) then - global.omagic.chemplants[entry_idx] = nil - log("MagicChemplantOnTick - Magic assembler entry removed?") - goto next_chemplant_entry - end - - local energy_share = entry.energy_input.energy/(#entry.chemplants + #entry.refineries) - - for idx,chemplant in pairs(entry.chemplants) do - - if (chemplant == nil) or (not chemplant.valid) then - global.omagic.chemplants[idx] = nil - log("Magic chemplant removed?") - goto next_chemplant_entry - end - - recipe = chemplant.get_recipe() - - if (not recipe) then - goto next_chemplant -- No recipe means do nothing. - end - - local energy_cost = recipe.energy * CHEMPLANT_ENERGY_PER_CRAFT_SECOND - if (energy_share < energy_cost) then goto next_chemplant end -- Not enough energy! - - local input_inv = chemplant.get_inventory(defines.inventory.assembling_machine_input) - local input_items = input_inv.get_contents() - local input_fluids = chemplant.get_fluid_contents() - - for _,v in ipairs(recipe.ingredients) do - if (not input_items[v.name] or (input_items[v.name] < v.amount)) then - if (not input_fluids[v.name] or (input_fluids[v.name] < v.amount)) then - goto next_chemplant -- Not enough ingredients - end - end - end - - local recipe_product = recipe.products[next(recipe.products)] -- Assume only 1 product. - - if recipe_product.type == "fluid" then - - if ((chemplant.get_fluid_count(recipe_product.name) + recipe_product.amount) > 100) then - goto next_chemplant -- Not enough space for ouput - end - - chemplant.insert_fluid({name=recipe_product.name, amount=recipe_product.amount}) - if (chemplant.last_user) then - chemplant.last_user.force.fluid_production_statistics.on_flow(recipe_product.name, recipe_product.amount) - end - - -- Otherwise it must be an item type - else - - local output_inv = chemplant.get_inventory(defines.inventory.assembling_machine_output) - - -- Can we insert at least 1 of the recipe result? - if not output_inv.can_insert({name=recipe_product.name, amount=recipe_product.amount}) then goto next_chemplant end - - -- Add recipe count to output - output_inv.insert({name=recipe_product.name, count=recipe_product.amount}) - if (chemplant.last_user) then - chemplant.last_user.force.item_production_statistics.on_flow(recipe_product.name, recipe_product.amount) - end - end - - -- Subtract ingredients from input - for _,v in ipairs(recipe.ingredients) do - if (input_items[v.name]) then - input_inv.remove({name=v.name, count=v.amount}) - if (chemplant.last_user) then - chemplant.last_user.force.item_production_statistics.on_flow(v.name, -v.amount) - end - elseif (input_fluids[v.name]) then - chemplant.remove_fluid{name=v.name, amount=v.amount} - if (chemplant.last_user) then - chemplant.last_user.force.fluid_production_statistics.on_flow(v.name, -v.amount) - end - end - end - - chemplant.products_finished = chemplant.products_finished + 1 - - -- Track energy usage - entry.energy_input.energy = entry.energy_input.energy - energy_cost - chemplant.surface.pollute(chemplant.position, recipe.energy*CHEMPLANT_POLLUTION_PER_CRAFT_SECOND) - - - ::next_chemplant:: - end - - ::next_chemplant_entry:: - end -end - - -function MagicRefineryOnTick() - if not global.omagic.refineries then return end - - for entry_idx,entry in pairs(global.omagic.refineries) do - - -- Validate the entry. - if (entry == nil) or (entry.refineries == nil) or (entry.energy_input == nil) or (not entry.energy_input.valid) then - global.omagic.refineries[entry_idx] = nil - log("MagicRefineryOnTick - Magic assembler entry removed?") - goto next_refinery_entry - end - - local energy_share = entry.energy_input.energy/(#entry.chemplants + #entry.refineries) - - for idx,refinery in pairs(entry.refineries) do - - if (refinery == nil) or (not refinery.valid) then - global.omagic.refineries[idx] = nil - log("Magic refinery removed?") - goto next_refinery_entry - end - - recipe = refinery.get_recipe() - - if (not recipe) then - goto next_refinery -- No recipe means do nothing. - end - - local energy_cost = recipe.energy * REFINERY_ENERGY_PER_CRAFT_SECOND - if (energy_share < energy_cost) then goto next_refinery end -- Not enough energy! - - local fluidbox_copy = refinery.fluidbox - - -- If recipe is COAL LIQUEFACTION: heavy(1), steam(2), heavy(3), light(4), petro(5) - -- if (recipe.name == "coal-liquefaction") then - - - -- If recipe is Advanced OIL: water(1), crude(2), heavy(3), light(4), petro(5) - if (recipe.name == "advanced-oil-processing") then - - if ((not refinery.fluidbox[1]) or (refinery.fluidbox[1].amount < 50)) then goto next_refinery end -- Not enough water - if ((not refinery.fluidbox[2]) or (refinery.fluidbox[2].amount < 100)) then goto next_refinery end -- Not enough crude - if ((refinery.fluidbox[3]) and (refinery.fluidbox[3].amount > 25)) then goto next_refinery end -- Not enough space for heavy - if ((refinery.fluidbox[4]) and (refinery.fluidbox[4].amount > 45)) then goto next_refinery end -- Not enough space for light - if ((refinery.fluidbox[5]) and (refinery.fluidbox[5].amount > 55)) then goto next_refinery end -- Not enough space for petro - - refinery.remove_fluid{name="water", amount=50} - refinery.remove_fluid{name="crude-oil", amount=100} - refinery.insert_fluid({name="heavy-oil", amount=25}) - refinery.insert_fluid({name="light-oil", amount=45}) - refinery.insert_fluid({name="petroleum-gas", amount=55}) - - if (refinery.last_user) then - refinery.last_user.force.fluid_production_statistics.on_flow("water", -50) - refinery.last_user.force.fluid_production_statistics.on_flow("crude-oil", -100) - refinery.last_user.force.fluid_production_statistics.on_flow("heavy-oil", 25) - refinery.last_user.force.fluid_production_statistics.on_flow("light-oil", 45) - refinery.last_user.force.fluid_production_statistics.on_flow("petroleum-gas", 55) - end - - -- If recipe is Basic OIL: crude(1), petro(2) - elseif (recipe.name == "basic-oil-processing") then - - if ((not refinery.fluidbox[1]) or (refinery.fluidbox[1].amount < 100)) then goto next_refinery end -- Not enough crude - if ((refinery.fluidbox[2]) and (refinery.fluidbox[2].amount > 45)) then goto next_refinery end -- Not enough space for petro - - refinery.remove_fluid{name="crude-oil", amount=100} - refinery.insert_fluid({name="petroleum-gas", amount=45}) - - if (refinery.last_user) then - refinery.last_user.force.fluid_production_statistics.on_flow("crude-oil", -100) - refinery.last_user.force.fluid_production_statistics.on_flow("petroleum-gas", 45) - end - - else - goto next_refinery -- Shouldn't hit this... - end - - refinery.products_finished = refinery.products_finished + 1 - - -- Track energy usage - entry.energy_input.energy = entry.energy_input.energy - energy_cost - refinery.surface.pollute(refinery.position, recipe.energy*REFINERY_POLLUTION_PER_CRAFT_SECOND) - - ::next_refinery:: - end - - ::next_refinery_entry:: - end -end - -function MagicAssemblerOnTick() - if not global.omagic.assemblers then return end - - for entry_idx,entry in pairs(global.omagic.assemblers) do - - -- Validate the entry. - if (entry == nil) or (entry.entities == nil) or (entry.energy_input == nil) or (not entry.energy_input.valid) then - global.omagic.assemblers[entry_idx] = nil - log("MagicAssemblerOnTick - Magic assembler entry removed?") - goto next_assembler_entry - end - - local energy_share = entry.energy_input.energy/#entry.entities - - for idx,assembler in pairs(entry.entities) do - - if (assembler == nil) or (not assembler.valid) then - global.omagic.assemblers[entry_idx] = nil - log("MagicAssemblerOnTick - Magic assembler removed?") - goto next_assembler_entry - end - - recipe = assembler.get_recipe() - - if (not recipe) then - goto next_assembler -- No recipe means do nothing. - end - - local energy_cost = recipe.energy * ASSEMBLER3_ENERGY_PER_CRAFT_SECOND - if (energy_share < energy_cost) then goto next_assembler end -- Not enough energy! - - -- Assume only 1 product and that it's an item! - local recipe_product = recipe.products[next(recipe.products)] - if recipe_product.type ~= "item" then goto next_assembler end - - local input_inv = assembler.get_inventory(defines.inventory.assembling_machine_input) - local input_items = input_inv.get_contents() - local input_fluids = assembler.get_fluid_contents() - - for _,v in ipairs(recipe.ingredients) do - if (not input_items[v.name] or (input_items[v.name] < v.amount)) then - if (not input_fluids[v.name] or (input_fluids[v.name] < v.amount)) then - goto next_assembler -- Not enough ingredients - end - end - end - - local output_inv = assembler.get_inventory(defines.inventory.assembling_machine_output) - if not output_inv.can_insert({name=recipe_product.name, amount=recipe_product.amount}) then - goto next_assembler -- Can we insert the result? - end - - -- Add recipe count to output - output_inv.insert({name=recipe_product.name, count=recipe_product.amount}) - if (assembler.last_user) then - assembler.last_user.force.item_production_statistics.on_flow(recipe_product.name, recipe_product.amount) - end - - -- Subtract ingredients from input - for _,v in ipairs(recipe.ingredients) do - if (input_items[v.name]) then - input_inv.remove({name=v.name, count=v.amount}) - if (assembler.last_user) then - assembler.last_user.force.item_production_statistics.on_flow(v.name, -v.amount) - end - elseif (input_fluids[v.name]) then - assembler.remove_fluid{name=v.name, amount=v.amount} - if (assembler.last_user) then - assembler.last_user.force.fluid_production_statistics.on_flow(v.name, -v.amount) - end - end - end - - -- Track energy usage - entry.energy_input.energy = entry.energy_input.energy - energy_cost - assembler.surface.pollute(assembler.position, recipe.energy*ASSEMBLER3_POLLUTION_PER_CRAFT_SECOND) - - assembler.products_finished = assembler.products_finished + 1 - - ::next_assembler:: - end - - ::next_assembler_entry:: - end -end - -function MagicCentrifugeOnTick() - if not global.omagic.centrifuges then return end - - for entry_idx,entry in pairs(global.omagic.centrifuges) do - - -- Validate the entry. - if (entry == nil) or (entry.entities == nil) or (entry.energy_input == nil) or (not entry.energy_input.valid) then - global.omagic.centrifuges[entry_idx] = nil - log("MagicCentrifugeOnTick - Magic centrifuge entry removed?") - goto next_centrifuge_entry - end - - local energy_share = entry.energy_input.energy/#entry.entities - - for idx,centrifuge in pairs(entry.entities) do - - if (centrifuge == nil) or (not centrifuge.valid) then - global.omagic.centrifuges[entry_idx] = nil - log("MagicCentrifugeOnTick - Magic centrifuge removed?") - goto next_centrifuge_entry - end - - recipe = centrifuge.get_recipe() - - if (not recipe) then - goto next_centrifuge -- No recipe means do nothing. - end - - local energy_cost = recipe.energy * CENTRIFUGE_ENERGY_PER_CRAFT_SECOND - if (energy_share < energy_cost) then goto next_centrifuge end -- Not enough energy! - - local input_inv = centrifuge.get_inventory(defines.inventory.assembling_machine_input) - local input_items = input_inv.get_contents() - - for _,v in ipairs(recipe.ingredients) do - if (not input_items[v.name] or (input_items[v.name] < v.amount)) then - goto next_centrifuge -- Not enough ingredients - end - end - - local output_inv = centrifuge.get_inventory(defines.inventory.assembling_machine_output) - - local output_item, output_count - - -- 10 uranium ore IN - -- .993 uranium-238 and .007 uranium-235 OUT - if (recipe.name == "uranium-processing") then - - local rand_chance = math.random() - - output_count = 1 - if (rand_chance <= .007) then - output_item = "uranium-235" - else - output_item = "uranium-238" - end - - -- Check if we can insert at least 1 of BOTH. - if not output_inv.can_insert({name="uranium-235", amount=output_count}) then - goto next_centrifuge - end - if not output_inv.can_insert({name= "uranium-238", amount=output_count}) then - goto next_centrifuge - end - - output_inv.insert({name=output_item, count=output_count}) - if (centrifuge.last_user) then - centrifuge.last_user.force.item_production_statistics.on_flow(output_item, output_count) - end - - for _,v in ipairs(recipe.ingredients) do - if (input_items[v.name]) then - input_inv.remove({name=v.name, count=v.amount}) - if (centrifuge.last_user) then - centrifuge.last_user.force.item_production_statistics.on_flow(v.name, -v.amount) - end - end - end - else - goto next_centrifuge -- Unsupported! - end - - centrifuge.products_finished = centrifuge.products_finished + 1 - - -- Track energy usage - entry.energy_input.energy = entry.energy_input.energy - energy_cost - centrifuge.surface.pollute(centrifuge.position, recipe.energy*CENTRIFUGE_POLLUTION_PER_CRAFT_SECOND) - - ::next_centrifuge:: - end - - ::next_centrifuge_entry:: - end -end - -COIN_MULTIPLIER = 2 - -COIN_GENERATION_CHANCES = { - ["small-biter"] = 0.01, - ["medium-biter"] = 0.02, - ["big-biter"] = 0.05, - ["behemoth-biter"] = 1, - - ["small-spitter"] = 0.01, - ["medium-spitter"] = 0.02, - ["big-spitter"] = 0.05, - ["behemoth-spitter"] = 1, - - ["small-worm-turret"] = 5, - ["medium-worm-turret"] = 10, - ["big-worm-turret"] = 15, - ["behemoth-worm-turret"] = 25, - - ["biter-spawner"] = 20, - ["spitter-spawner"] = 20, -} - -function CoinsFromEnemiesOnPostEntityDied(event) - if (not event.prototype or not event.prototype.name) then return end - - local coin_chance = nil - if (COIN_GENERATION_CHANCES[event.prototype.name]) then - coin_chance = COIN_GENERATION_CHANCES[event.prototype.name] - end - - if (coin_chance) then - DropCoins(event.position, coin_chance, event.force) - end -end - --- Drop coins, force is optional, decon is applied if force is not nil. -function DropCoins(pos, count, force) - - local drop_amount = 0 - - -- If count is less than 1, it represents a probability to drop a single coin - if (count < 1) then - if (math.random() < count) then - drop_amount = 1 - end - - -- If count is 1 or more, it represents a probability to drop at least that amount and up to 3x - elseif (count >= 1) then - drop_amount = math.random(count,count*COIN_MULTIPLIER) - end - - if drop_amount == 0 then return end - game.surfaces[GAME_SURFACE_NAME].spill_item_stack(pos, {name="coin", count=math.floor(drop_amount)}, true, force, false) -- Set nil to force to auto decon. -end \ No newline at end of file diff --git a/lib/notepad.lua b/lib/notepad.lua deleted file mode 100644 index 79dc769..0000000 --- a/lib/notepad.lua +++ /dev/null @@ -1,35 +0,0 @@ --- notepad.lua --- Oarc's simple notepad cause I keep forgetting what I want to do next. - - -function CreateNotepadGuiTab(tab_container, player) - - if global.oarc_notepad == nil then - global.oarc_notepad = {} - end - - if global.oarc_notepad[player.name] == nil then - global.oarc_notepad[player.name] = "Write something here...!" - end - - AddLabel(tab_container, "notepad_info", "Use this to take notes:", my_longer_label_style) - - local txt_box = tab_container.add{type="text-box", name="oarc_notepad_textbox", text=global.oarc_notepad[player.name]} - ApplyStyle(txt_box, my_notepad_fixed_width_style) - - txt_box.focus() -end - - -function NotepadOnGuiTextChange(event) - - if (event.element.name ~= "oarc_notepad_textbox") then return end - - local player = game.players[event.player_index] - - if global.oarc_notepad == nil then - global.oarc_notepad = {} - end - - global.oarc_notepad[player.name] = event.element.text -end diff --git a/lib/oarc_buy.lua b/lib/oarc_buy.lua deleted file mode 100644 index 5f627a5..0000000 --- a/lib/oarc_buy.lua +++ /dev/null @@ -1,283 +0,0 @@ --- oarc_buy.lua --- May 2020 --- Adding microtransactions. - -require("lib/oarc_store_player_items") -require("lib/oarc_store_map_features") -local mod_gui = require("mod-gui") - --- NAME of the top level element (outer frame) -OARC_STORE_GUI = "oarc_store_gui" - -OARC_PLAYER_STORE_GUI_TAB_NAME = "Item Store" -OARC_MAP_FEATURE_GUI_TAB_NAME = "Special Store" - -local OARC_STORE_TAB_CONTENT_FUNCTIONS = {} -OARC_STORE_TAB_CONTENT_FUNCTIONS[OARC_PLAYER_STORE_GUI_TAB_NAME] = CreatePlayerStoreTab -OARC_STORE_TAB_CONTENT_FUNCTIONS[OARC_MAP_FEATURE_GUI_TAB_NAME] = CreateMapFeatureStoreTab - -function InitOarcStoreGuiTabs(player) - CreateOarcStoreButton(player) - CreateOarcStoreTabsPane(player) - - -- Store for personal items - AddOarcStoreTab(player, OARC_PLAYER_STORE_GUI_TAB_NAME) - SetOarcStoreTabEnabled(player, OARC_PLAYER_STORE_GUI_TAB_NAME, true) - - -- Store for map feature stuff - AddOarcStoreTab(player, OARC_MAP_FEATURE_GUI_TAB_NAME) - SetOarcStoreTabEnabled(player, OARC_MAP_FEATURE_GUI_TAB_NAME, true) - - HideOarcStore(player) -end - -function CreateOarcStoreButton(player) - if (mod_gui.get_button_flow(player).oarc_store == nil) then - local b = mod_gui.get_button_flow(player).add{name="oarc_store", - type="sprite-button", - sprite="item/coin", - style=mod_gui.button_style} - b.style.padding=2 - end -end - -function DoesOarcStoreExist(player) - return (mod_gui.get_frame_flow(player)[OARC_STORE_GUI] ~= nil) -end - -function IsOarcStoreVisible(player) - local of = mod_gui.get_frame_flow(player)[OARC_STORE_GUI] - return (of.visible) -end - -function ShowOarcStore(player) - local of = mod_gui.get_frame_flow(player)[OARC_STORE_GUI] - if (of == nil) then return end - of.visible = true - player.opened = of -end - -function HideOarcStore(player) - local of = mod_gui.get_frame_flow(player)[OARC_STORE_GUI] - if (of == nil) then return end - of.visible = false - player.opened = nil -end - -function GetOarcStoreTabsPane(player) - if (mod_gui.get_frame_flow(player)[OARC_STORE_GUI] == nil) then - return nil - else - return mod_gui.get_frame_flow(player)[OARC_STORE_GUI].store_if.store_tabs - end -end - -function ClickOarcStoreButton(event) - if not (event and event.element and event.element.valid) then return end - local button = event.element - local player = game.players[event.player_index] - - -- Don't allow any clicks on the store while player is dead! - if (not player or player.ticks_to_respawn) then - if (DoesOarcStoreExist(player)) then - HideOarcStore(player) - end - return - end - - if (button.name == "oarc_store") then - if (not DoesOarcStoreExist(player)) then - CreateOarcStoreTabsPane(player) - else - if (IsOarcStoreVisible(player)) then - HideOarcStore(player) - else - ShowOarcStore(player) - FakeTabChangeEventOarcStore(player) - end - end - elseif ((button.parent ~= nil) and (button.parent.parent ~= nil)) then - if (button.parent.parent.name == OARC_PLAYER_STORE_GUI_TAB_NAME.."_if") then - OarcPlayerStoreButton(event) - elseif (button.parent.parent.name == OARC_MAP_FEATURE_GUI_TAB_NAME.."_if") then - OarcMapFeatureStoreButton(event) - end - end -end - -function TabChangeOarcStore(event) - if (event.element.name ~= "store_tabs") then return end - - local player = game.players[event.player_index] - local otabs = event.element - local selected_tab_name = otabs.tabs[otabs.selected_tab_index].tab.name - - -- Clear all tab contents - for i,t in pairs(otabs.tabs) do - t.content.clear() - end - - SetOarcStoreTabContent(player, selected_tab_name) -end - -function FakeTabChangeEventOarcStore(player) - local event = {} - event.element = GetOarcStoreTabsPane(player) - event.player_index = player.index - TabChangeOarcStore(event) -end - -function CreateOarcStoreTabsPane(player) - if (mod_gui.get_frame_flow(player)[OARC_STORE_GUI] == nil) then - - -- OUTER FRAME (TOP GUI ELEMENT) - local frame = mod_gui.get_frame_flow(player).add{ - type = 'frame', - name = OARC_STORE_GUI, - direction = "vertical"} - frame.style.padding = 5 - - -- INNER FRAME - local inside_frame = frame.add{ - type = "frame", - name = "store_if", - style = "inside_deep_frame", - direction = "vertical" - } - - -- SUB HEADING w/ LABEL - local subhead = inside_frame.add{ - type="frame", - name="sub_header", - style = "changelog_subheader_frame", - direction = "vertical"} - AddLabel(subhead, "store_info", "OARC Microtransactions and DLC", "subheader_caption_label") - - -- TABBED PANE - local store_tabs = inside_frame.add{ - name="store_tabs", - type="tabbed-pane", - style="tabbed_pane"} - store_tabs.style.top_padding = 8 - end -end - -function AddOarcStoreTab(player, tab_name) - -- if (not DoesOarcStoreExist(player)) then - -- CreateOarcStoreTabsPane(player) - -- end - - -- Get the tabbed pane - local otabs = GetOarcStoreTabsPane(player) - - -- Create new tab - local new_tab = otabs.add{ - type="tab", - name=tab_name, - caption=tab_name} - - -- Create inside frame for content - local tab_inside_frame = otabs.add{ - type="frame", - name=tab_name.."_if", - style = "inside_deep_frame", - direction="vertical"} - tab_inside_frame.style.left_margin = 10 - tab_inside_frame.style.right_margin = 10 - tab_inside_frame.style.top_margin = 4 - tab_inside_frame.style.bottom_margin = 4 - tab_inside_frame.style.padding = 5 - tab_inside_frame.style.horizontally_stretchable = true - -- tab_inside_frame.style.vertically_stretchable = true - -- tab_inside_frame.style.horizontally_squashable = true - -- tab_inside_frame.style.vertically_squashable = true - - -- Add the whole thing to the tab now. - otabs.add_tab(new_tab, tab_inside_frame) - - -- Disable all new tabs by default - new_tab.enabled = false - - -- If no other tabs are selected, select the first one. - if (otabs.selected_tab_index == nil) then - otabs.selected_tab_index = 1 - end -end - -function SetOarcStoreTabContent(player, tab_name) - if (not DoesOarcStoreExist(player)) then return end - - local otabs = GetOarcStoreTabsPane(player) - - for _,t in ipairs(otabs.tabs) do - if (t.tab.name == tab_name) then - t.content.clear() - OARC_STORE_TAB_CONTENT_FUNCTIONS[tab_name](t.content, player) - return - end - end -end - -function SetOarcStoreTabEnabled(player, tab_name, enable) - if (not DoesOarcStoreExist(player)) then return end - - local otabs = GetOarcStoreTabsPane(player) - - for _,t in ipairs(otabs.tabs) do - if (t.tab.name == tab_name) then - t.tab.enabled = enable - return - end - end -end - -function OarcStoreOnGuiClosedEvent(event) - if (event.element and (event.element.name == OARC_STORE_GUI)) then - HideOarcStore(game.players[event.player_index]) - end -end - -commands.add_command("donate-coins", "Toss a Coin to Your Witcher", function(command) - - local player = game.players[command.player_index] - - if (command.parameter == nil) then - player.print("Invalid parameters? /donate-coins [username] [amount]") - return - end - - local target, amount - local count = 1 - for i in string.gmatch(command.parameter, "%S+") do - if (count == 1) then - target = i - end - if (count == 2) then - amount = i - end - count = count + 1 - end - - if (count ~= 3) then - player.print("Invalid parameters (count = " ..count..")? /donate-coins [username] [amount]") - return - end - - -- Validate all the things... - if (game.players[target] and - not game.players[target].ticks_to_respawn and - amount and - player and - player.get_main_inventory()) then - local target_player = game.players[target] - local amount_number = tonumber(amount) - if ((amount_number > 0) and (player.get_main_inventory().get_item_count("coin") >= amount_number)) then - local transfer = target_player.get_main_inventory().insert({name="coin", count=amount_number}) - player.get_main_inventory().remove({name="coin", count=transfer}) - player.print("You transfered " .. transfer .. " coins to " .. target .. ".") - target_player.print("You received " .. transfer .. " coins from " .. player.name .. ".") - else - player.print("You can't transfer what you don't have... (Not enough coins!)") - end - end -end) diff --git a/lib/oarc_enemies.lua b/lib/oarc_enemies.lua deleted file mode 100644 index a0591f7..0000000 --- a/lib/oarc_enemies.lua +++ /dev/null @@ -1,163 +0,0 @@ --- oarc_enemies.lua --- Feb 2020 - --- This is my second attempt at modifying the normal enemy experience. The --- first attempt ended up in a wave attack system which wasn't well received. --- This attempt will try to intercept normal vanilla enemy groups and modify --- them based on player activity. - --- Basic logic: --- on_unit_group_finished_gathering we check what command is given. --- find destination position --- check for closest "player" using find_nearest_enemy function --- if a player is found, check if player is part of a shared spawn --- Remove the enemy group if no player in the shared spawn is online. - --- TODO: --- Add options for modifying the default waves or spawning additional special waves. --- Add option to disable attacks completely for a given spawn. - --- Generic Utility Includes -require("lib/oarc_utils") - -function OarcModifyEnemyGroup(group) - - -- Check validity - if ((group == nil) or (group.command == nil) or (group.force.name ~= "enemy")) then - log("OarcModifyEnemyGroup ignoring INVALID group/command") - return - end - - -- Make sure the attack is of a TYPE that we care about. - if ((group.command.type == defines.command.attack) or - (group.command.type == defines.command.attack_area) or - (group.command.type == defines.command.build_base)) then - log("OarcModifyEnemyGroup MODIFYING command TYPE=" .. group.command.type) - else - log("OarcModifyEnemyGroup ignoring command TYPE=" .. group.command.type) - return - end - - -- defines.command.attack --> target --> target.position - if (group.command.type == defines.command.attack) then - log("OarcModifyEnemyGroup defines.command.attack NOT IMPLEMENTED YET!") - return - - -- defines.command.attack_area --> destination --> closest enemy (within 3 chunk radius?) - -- defines.command.build_base --> destination --> closest enemy (expansion chunk distance?) - else - local destination = group.command.destination - - local distance = CHUNK_SIZE*3 - if (group.command.type == defines.command.build_base) then - distance = CHUNK_SIZE*(game.map_settings.enemy_expansion.max_expansion_distance) - end - - -- Find some enemies near the attack point. - local target_entities = group.surface.find_entities_filtered{ - position=destination, - radius=distance, - force={"enemy", "neutral"}, - limit=50, - invert=true} - - -- Search through them all to find anything with a last_user. - local target_entity = nil - for _,target in ipairs(target_entities) do - if (target.last_user ~= nil) then - target_entity = target - break - end - end - - -- No enemies nearby? - if (target_entity == nil) then - if (group.command.type == defines.command.attack_area) then - if (global.enable_oe_debug) then - SendBroadcastMsg("OarcModifyEnemyGroup find_nearest_enemy attack_area FAILED!?!? " .. GetGPStext(group.position) .. " Target: " .. GetGPStext(destination)) - end - log("OarcModifyEnemyGroup UNEXPECTED find_nearest_enemy did not find anything!") - for _,member in pairs(group.members) do - member.destroy() - end - else - log("OarcModifyEnemyGroup find_nearest_enemy did not find anything!") - end - return - end - - -- Probably don't need this I hope? - if (target_entity.force == "neutral") then - log("OarcModifyEnemyGroup UNEXPECTED find_nearest_enemy found neutral target?") - for _,member in pairs(group.members) do - member.destroy() - end - return - end - - -- Most common target will be a built entity with a "last_user" - local target_player = target_entity.last_user - - -- Target could also be a player character (more rare) - if (target_player == nil) and (target_entity.type == "character") then - target_player = target_entity.player - end - - -- I don't think this should happen... - if ((target_player == nil) or (not target_player.valid)) then - if (global.enable_oe_debug) then - SendBroadcastMsg("ERROR?? target_player nil/invalid " .. GetGPStext(group.position) .. " Target: " .. GetGPStext(target_entity.position)) - end - log("OarcModifyEnemyGroup ERROR?? target_player nil/invalid") - for _,member in pairs(group.members) do - member.destroy() - end - return - end - - -- Is the target player online? Then the attack can go through. - if (target_player.connected) then - if (global.enable_oe_debug) then - SendBroadcastMsg("Enemy group released (player): " .. GetGPStext(group.position) .. " Target: " .. GetGPStext(target_entity.position) .. " " .. target_player.name) - end - log("OarcModifyEnemyGroup RELEASING enemy group since player is ONLINE") - return - end - - -- Find the shared spawn that the player is part of. - -- This could be the own player's spawn (quite likely) - local sharedSpawnOwnerName = FindPlayerSharedSpawn(target_player.name) - - -- Is someone in the shared spawn online? - if (sharedSpawnOwnerName ~= nil) then - if (GetOnlinePlayersAtSharedSpawn(sharedSpawnOwnerName) > 0) then - if (global.enable_oe_debug) then - SendBroadcastMsg("Enemy group released (shared): " .. GetGPStext(group.position) .. " Target: " .. GetGPStext(target_entity.position) .. " " .. target_player.name) - end - log("OarcModifyEnemyGroup RELEASING enemy group since someone in the group is ONLINE") - return - end - end - - -- Is there a buddy spawn and is the buddy online? - local buddyName = global.ocore.buddyPairs[sharedSpawnOwnerName] - if (buddyName ~= nil) and (game.players[buddyName] ~= nil) then - if (game.players[buddyName].connected or (GetOnlinePlayersAtSharedSpawn(buddyName) > 0)) then - if (global.enable_oe_debug) then - SendBroadcastMsg("Enemy group released (buddy): " .. GetGPStext(group.position) .. " Target: " .. GetGPStext(target_entity.position) .. " " .. target_player.name) - end - log("OarcModifyEnemyGroup RELEASING enemy group since someone in the BUDDY PAIR is ONLINE") - return - end - end - - -- Otherwise, we delete the group. - if (global.enable_oe_debug) then - SendBroadcastMsg("Enemy group deleted: " .. GetGPStext(group.position) .. " Target: " .. GetGPStext(target_entity.position) .. " " .. target_player.name) - end - for _,member in pairs(group.members) do - member.destroy() - end - log("OarcModifyEnemyGroup REMOVED enemy group since nobody was online?") - end -end \ No newline at end of file diff --git a/lib/oarc_global_cfg.lua b/lib/oarc_global_cfg.lua deleted file mode 100644 index f767fe8..0000000 --- a/lib/oarc_global_cfg.lua +++ /dev/null @@ -1,102 +0,0 @@ --- oarc_global_cfg.lua --- April 2019 --- --- Here is where we store/init config values to the global table. --- Allows runtime modification of game settings if we want it. --- Also allows supporting both MOD and SCENARIO versions. - --- DON'T JUDGE ME - - --- That's a LOT of settings. -function InitOarcConfig() - - global.ocfg = {} - - if (game.active_mods["clean-tutorial-grid"]) then - global.ocfg.locked_build_area_tile = "clean-tutorial-grid" - else - global.ocfg.locked_build_area_tile = "tutorial-grid" - end - - -- SCENARIO VERSION (ONLY - no more mod version.) - global.ocfg.welcome_title = WELCOME_MSG_TITLE - global.ocfg.welcome_msg = WELCOME_MSG - global.ocfg.server_rules = SERVER_MSG - global.ocfg.minimum_online_time = MIN_ONLINE_TIME_IN_MINUTES - global.ocfg.server_contact = CONTACT_MSG - global.ocfg.enable_vanilla_spawns = ENABLE_VANILLA_SPAWNS - global.ocfg.enable_buddy_spawn = ENABLE_BUDDY_SPAWN - global.ocfg.frontier_rocket_silo = FRONTIER_ROCKET_SILO_MODE - global.ocfg.silo_islands = SILO_ISLANDS_MODE - global.ocfg.enable_undecorator = ENABLE_UNDECORATOR - global.ocfg.enable_tags = ENABLE_TAGS - global.ocfg.enable_long_reach = ENABLE_LONGREACH - global.ocfg.enable_autofill = ENABLE_AUTOFILL - global.ocfg.enable_miner_decon = ENABLE_MINER_AUTODECON - global.ocfg.enable_player_list = ENABLE_PLAYER_LIST - global.ocfg.list_offline_players = PLAYER_LIST_OFFLINE_PLAYERS - global.ocfg.enable_shared_team_vision = ENABLE_SHARED_TEAM_VISION - global.ocfg.enable_regrowth = ENABLE_REGROWTH - global.ocfg.enable_abandoned_base_removal = ENABLE_ABANDONED_BASE_REMOVAL - global.ocfg.enable_research_queue = ENABLE_RESEARCH_QUEUE - global.ocfg.enable_coin_shop = ENABLE_COIN_SHOP - global.ocfg.enable_chest_sharing = ENABLE_ITEM_AND_ENERGY_SHARING - global.ocfg.enable_magic_factories = ENABLE_MAGIC_FACTORIES - global.ocfg.enable_offline_protect = ENABLE_OFFLINE_PROTECTION - global.ocfg.enable_power_armor_start = ENABLE_POWER_ARMOR_QUICK_START - global.ocfg.enable_modular_armor_start = ENABLE_MODULAR_ARMOR_QUICK_START - global.ocfg.lock_goodies_rocket_launch = LOCK_GOODIES_UNTIL_ROCKET_LAUNCH - global.ocfg.scale_resources_around_spawns = SCALE_RESOURCES_AROUND_SPAWNS - - global.ocfg.modified_enemy_spawning = OARC_MODIFIED_ENEMY_SPAWNING - global.ocfg.near_dist_start = NEAR_MIN_DIST - global.ocfg.near_dist_end = NEAR_MAX_DIST - global.ocfg.far_dist_start = FAR_MIN_DIST - global.ocfg.far_dist_end = FAR_MAX_DIST - global.ocfg.vanilla_spawn_count = VANILLA_SPAWN_COUNT - global.ocfg.vanilla_spawn_spacing = VANILLA_SPAWN_SPACING - - global.ocfg.spawn_config = OARC_CFG - - global.ocfg.enable_separate_teams = ENABLE_SEPARATE_TEAMS - global.ocfg.main_force = MAIN_FORCE - global.ocfg.enable_shared_spawns = ENABLE_SHARED_SPAWNS - global.ocfg.max_players_shared_spawn = MAX_PLAYERS_AT_SHARED_SPAWN - global.ocfg.enable_shared_chat = ENABLE_SHARED_TEAM_CHAT - global.ocfg.respawn_cooldown_min = RESPAWN_COOLDOWN_IN_MINUTES - global.ocfg.frontier_silo_count = SILO_NUM_SPAWNS - global.ocfg.frontier_silo_distance = SILO_CHUNK_DISTANCE - global.ocfg.frontier_fixed_pos = SILO_FIXED_POSITION - global.ocfg.frontier_pos_table = SILO_POSITIONS - global.ocfg.frontier_silo_vision = ENABLE_SILO_VISION - global.ocfg.frontier_allow_build = ENABLE_SILO_PLAYER_BUILD - - global.ocfg.enable_anti_grief = ENABLE_ANTI_GRIEFING - global.ocfg.ghost_ttl = GHOST_TIME_TO_LIVE - global.ocfg.enable_friendly_fire = ENABLE_FRIENDLY_FIRE - - global.ocfg.enable_server_write_files = ENABLE_SERVER_WRITE_FILES - - - ----------------------- - -- VALIDATION CHECKS -- - ----------------------- - - if (not global.ocfg.frontier_rocket_silo or not global.ocfg.enable_vanilla_spawns) then - global.ocfg.silo_islands = false - end - - if (global.ocfg.enable_vanilla_spawns) then - global.ocfg.enable_buddy_spawn = false - end - - if (not global.ocfg.enable_coin_shop) then - global.ocfg.enable_chest_sharing = false - end - - if (not global.ocfg.enable_chest_sharing) then - global.ocfg.enable_magic_factories = false - end - -end \ No newline at end of file diff --git a/lib/oarc_gui_tabs.lua b/lib/oarc_gui_tabs.lua index 0cbc1ca..4703383 100644 --- a/lib/oarc_gui_tabs.lua +++ b/lib/oarc_gui_tabs.lua @@ -1,6 +1,10 @@ --- oarc_gui_tabs.lua +-- A nice way to organize the GUI tabs. local mod_gui = require("mod-gui") +require("lib/gui_tabs/server_info") +require("lib/gui_tabs/spawn_controls") +require("lib/gui_tabs/settings_controls") +require("lib/gui_tabs/mod_info_faq") -------------------------------------------------------------------------------- -- GUI Tab Handler @@ -10,117 +14,121 @@ local mod_gui = require("mod-gui") OARC_GUI = "oarc_gui" -- LIST of all implemented tabs and their content Functions -OARC_GAME_OPTS_GUI_TAB_NAME = "Server Info" -OARC_SPAWN_CTRL_GUI_NAME = "Spawn Controls" -OARC_PLAYER_LIST_GUI_TAB_NAME = "Players" -OARC_TAGS_GUI_TAB_NAME = "Name Tags" -OARC_ROCKETS_GUI_TAB_NAME = "Rockets" -OARC_SHARED_ITEMS_GUI_TAB_NAME = "Shared Items" -OARC_NOTEPAD_GUI_TAB_NAME = "Notepad" - -local OARC_GUI_TAB_CONTENT_FUNCTIONS = {} -OARC_GUI_TAB_CONTENT_FUNCTIONS["Server Info"] = CreateGameOptionsTab -OARC_GUI_TAB_CONTENT_FUNCTIONS["Spawn Controls"] = CreateSpawnCtrlGuiTab -OARC_GUI_TAB_CONTENT_FUNCTIONS["Players"] = CreatePlayerListGuiTab -OARC_GUI_TAB_CONTENT_FUNCTIONS["Name Tags"] = CreateTagGuiTab -OARC_GUI_TAB_CONTENT_FUNCTIONS["Rockets"] = CreateRocketGuiTab -OARC_GUI_TAB_CONTENT_FUNCTIONS["Shared Items"] = CreateSharedItemsGuiTab -OARC_GUI_TAB_CONTENT_FUNCTIONS["Notepad"] = CreateNotepadGuiTab - +OARC_SERVER_INFO_TAB_NAME = "server_info" +OARC_SPAWN_CTRL_TAB_NAME = "spawn_controls" +OARC_CONFIG_CTRL_TAB_NAME = "settings" +OARC_MOD_INFO_CTRL_TAB_NAME = "mod_info" + +OARC_SERVER_INFO_TAB_LOCALIZED = {"oarc-server-info-tab-title"} +OARC_SPAWN_CTRL_TAB_LOCALIZED = {"oarc-spawn-ctrls-tab-title"} +OARC_CONFIG_CTRL_TAB_LOCALIZED = {"oarc-settings-tab-title"} +OARC_MOD_INFO_CTRL_TAB_LOCALIZED = {"oarc-mod-info-tab-title"} + +local OARC_GUI_TAB_CONTENT_FUNCTIONS = { + [OARC_SERVER_INFO_TAB_NAME] = CreateServerInfoTab, + [OARC_SPAWN_CTRL_TAB_NAME] = CreateSpawnControlsTab, + [OARC_MOD_INFO_CTRL_TAB_NAME] = CreateModInfoTab, + [OARC_CONFIG_CTRL_TAB_NAME] = CreateSettingsControlsTab, +} + +---@param player LuaPlayer +---@return nil function InitOarcGuiTabs(player) + + -- Make safe to call multiple times + if (DoesOarcGuiExist(player)) then + return + end + CreateOarcGuiButton(player) -- Add general info tab - AddOarcGuiTab(player, OARC_GAME_OPTS_GUI_TAB_NAME) - SetOarcGuiTabEnabled(player, OARC_GAME_OPTS_GUI_TAB_NAME, true) + AddOarcGuiTab(player, OARC_SERVER_INFO_TAB_NAME, OARC_SERVER_INFO_TAB_LOCALIZED) + SetOarcGuiTabEnabled(player, OARC_SERVER_INFO_TAB_NAME, true) -- Spawn control tab, disabled by default - AddOarcGuiTab(player, OARC_SPAWN_CTRL_GUI_NAME) + AddOarcGuiTab(player, OARC_SPAWN_CTRL_TAB_NAME, OARC_SPAWN_CTRL_TAB_LOCALIZED) - -- If player list is enabled, create that - if global.ocfg.enable_player_list then - AddOarcGuiTab(player, OARC_PLAYER_LIST_GUI_TAB_NAME) - SetOarcGuiTabEnabled(player, OARC_PLAYER_LIST_GUI_TAB_NAME, true) - end + -- Regrowth control tab + AddOarcGuiTab(player, OARC_MOD_INFO_CTRL_TAB_NAME, OARC_MOD_INFO_CTRL_TAB_LOCALIZED) + SetOarcGuiTabEnabled(player, OARC_MOD_INFO_CTRL_TAB_NAME, true) - -- Player tags - if global.ocfg.enable_tags then - AddOarcGuiTab(player, OARC_TAGS_GUI_TAB_NAME) - SetOarcGuiTabEnabled(player, OARC_TAGS_GUI_TAB_NAME, true) - end + -- Settings control tab + AddOarcGuiTab(player, OARC_CONFIG_CTRL_TAB_NAME, OARC_CONFIG_CTRL_TAB_LOCALIZED) + SetOarcGuiTabEnabled(player, OARC_CONFIG_CTRL_TAB_NAME, true) - -- Rockets tab, only enable if one has been launched already - AddOarcGuiTab(player, OARC_ROCKETS_GUI_TAB_NAME) - if (global.ocore.satellite_sent) then - SetOarcGuiTabEnabled(player, OARC_ROCKETS_GUI_TAB_NAME, true) - end - - if global.ocfg.enable_chest_sharing then - AddOarcGuiTab(player, OARC_SHARED_ITEMS_GUI_TAB_NAME) - SetOarcGuiTabEnabled(player, OARC_SHARED_ITEMS_GUI_TAB_NAME, true) - end - - AddOarcGuiTab(player, OARC_NOTEPAD_GUI_TAB_NAME) - SetOarcGuiTabEnabled(player, OARC_NOTEPAD_GUI_TAB_NAME, true) - HideOarcGui(player) end +---@param player LuaPlayer +---@return nil function CreateOarcGuiButton(player) - if (mod_gui.get_button_flow(player).oarc_button == nil) then - local b = mod_gui.get_button_flow(player).add{name="oarc_button", - caption="CLICK ME FOR MORE INFO", - type="sprite-button", - -- sprite="utility/expand_dots", - style=mod_gui.button_style} - b.style.padding=2 - -- b.style.width=20 + if (mod_gui.get_button_flow(player).oarc_mod_gui_button == nil) then + local b = mod_gui.get_button_flow(player).add{ + name="oarc_mod_gui_button", + type="sprite-button", + sprite = "oarc-mod-sprite-40", + style="slot_button", + tooltip={ "oarc-gui-tooltip" } + } + b.style.padding=0 end end +---@param player LuaPlayer +---@return boolean function DoesOarcGuiExist(player) return (mod_gui.get_frame_flow(player)[OARC_GUI] ~= nil) end +---@param player LuaPlayer +---@return boolean function IsOarcGuiVisible(player) + ---@type LuaGuiElement local of = mod_gui.get_frame_flow(player)[OARC_GUI] return (of.visible) end +---@param player LuaPlayer +---@return nil function ShowOarcGui(player) + ---@type LuaGuiElement local of = mod_gui.get_frame_flow(player)[OARC_GUI] if (of == nil) then return end of.visible = true player.opened = of end +---@param player LuaPlayer +---@return nil function HideOarcGui(player) + ---@type LuaGuiElement local of = mod_gui.get_frame_flow(player)[OARC_GUI] if (of == nil) then return end of.visible = false player.opened = nil end +---@param player LuaPlayer +---@return LuaGuiElement? function GetOarcGuiTabsPane(player) - if (mod_gui.get_frame_flow(player)[OARC_GUI] == nil) then + ---@type LuaGuiElement + local of = mod_gui.get_frame_flow(player)[OARC_GUI] + if (of == nil) then return nil else - return mod_gui.get_frame_flow(player)[OARC_GUI].oarc_if.oarc_tabs + return of.oarc_if.oarc_tabs end end +---@param event EventData.on_gui_click +---@return nil function ClickOarcGuiButton(event) - if not (event and event.element and event.element.valid) then return end + if not event.element.valid then return end local player = game.players[event.player_index] local name = event.element.name - if (name ~= "oarc_button") then return end - - if (event.element.caption ~= "") then - event.element.caption = "" - event.element.style.width = 20 - event.element.sprite="utility/expand_dots" - end + if (name ~= "oarc_mod_gui_button") then return end if (not DoesOarcGuiExist(player)) then CreateOarcGuiTabsPane(player) @@ -129,33 +137,48 @@ function ClickOarcGuiButton(event) HideOarcGui(player) else ShowOarcGui(player) - FakeTabChangeEventOarcGui(player) + OarcGuiCreateContentOfTab(player) end end end -function TabChangeOarcGui(event) +---@param event EventData.on_gui_selected_tab_changed +---@return nil +function OarcGuiSelectedTabChanged(event) if (event.element.name ~= "oarc_tabs") then return end + OarcGuiCreateContentOfTab(game.players[event.player_index]) +end - local player = game.players[event.player_index] - local otabs = event.element - local selected_tab_name = otabs.tabs[otabs.selected_tab_index].tab.name +---Set tab content to currently selected tab, clears all other tab content and refreshes the selected tab content! +---Safe to call just to refresh the current tab. +---@param player LuaPlayer +---@return nil +function OarcGuiCreateContentOfTab(player) + local otabs = GetOarcGuiTabsPane(player) + if (otabs == nil) then return end - -- Clear all tab contents - for i,t in pairs(otabs.tabs) do + local tab_name = otabs.tabs[otabs.selected_tab_index].tab.name + + -- log("OarcGuiCreateContentOfTab: " .. tab_name) + + for _,t in ipairs(otabs.tabs) do t.content.clear() + if (t.tab.name == tab_name) then + OARC_GUI_TAB_CONTENT_FUNCTIONS[tab_name](t.content, player) + end end - - SetOarGuiTabContent(player, selected_tab_name) end -function FakeTabChangeEventOarcGui(player) - local event = {} - event.element = GetOarcGuiTabsPane(player) - event.player_index = player.index - TabChangeOarcGui(event) +---Just an alias for OarcGuiCreateContentOfTab +---@param player LuaPlayer +---@return nil +function OarcGuiRefreshContent(player) + -- log("Hit OarcGuiRefreshContent" .. player.name) + OarcGuiCreateContentOfTab(player) end +---@param player LuaPlayer +---@return nil function CreateOarcGuiTabsPane(player) if (mod_gui.get_frame_flow(player)[OARC_GUI] == nil) then @@ -179,8 +202,9 @@ function CreateOarcGuiTabsPane(player) local subhead = inside_frame.add{ type="frame", name="sub_header", - style = "changelog_subheader_frame"} - AddLabel(subhead, "scen_info", "Scenario Info and Controls", "subheader_caption_label") + style = "changelog_subheader_frame" + } + AddLabel(subhead, nil, {"oarc-gui-tab-header-label"}, "subheader_caption_label") -- TABBED PANE local oarc_tabs = inside_frame.add{ @@ -193,7 +217,10 @@ end -- Function creates a new tab. -- It adds whatever it wants to the provided scroll-pane. -function AddOarcGuiTab(player, tab_name) +---@param player LuaPlayer +---@param tab_name string +---@param localized_name LocalisedString +function AddOarcGuiTab(player, tab_name, localized_name) if (not DoesOarcGuiExist(player)) then CreateOarcGuiTabsPane(player) end @@ -201,11 +228,13 @@ function AddOarcGuiTab(player, tab_name) -- Get the tabbed pane local otabs = GetOarcGuiTabsPane(player) + if (otabs == nil) then return end + -- Create new tab local new_tab = otabs.add{ type="tab", name=tab_name, - caption=tab_name} + caption=localized_name} -- Create inside frame for content local tab_inside_frame = otabs.add{ @@ -235,21 +264,10 @@ function AddOarcGuiTab(player, tab_name) end end - -function SetOarGuiTabContent(player, tab_name) - if (not DoesOarcGuiExist(player)) then return end - - local otabs = GetOarcGuiTabsPane(player) - - for _,t in ipairs(otabs.tabs) do - if (t.tab.name == tab_name) then - t.content.clear() - OARC_GUI_TAB_CONTENT_FUNCTIONS[tab_name](t.content, player) - return - end - end -end - +---This sets the enable state of a tab. +---@param player LuaPlayer +---@param tab_name string +---@param enable boolean function SetOarcGuiTabEnabled(player, tab_name, enable) if (not DoesOarcGuiExist(player)) then return end @@ -263,6 +281,9 @@ function SetOarcGuiTabEnabled(player, tab_name, enable) end end +---Switches the tab to the one specified. +---@param player LuaPlayer +---@param tab_name string function SwitchOarcGuiTab(player, tab_name) if (not DoesOarcGuiExist(player)) then return end @@ -271,14 +292,15 @@ function SwitchOarcGuiTab(player, tab_name) for i,t in pairs(otabs.tabs) do if (t.tab.name == tab_name) then otabs.selected_tab_index = i - FakeTabChangeEventOarcGui(player) + OarcGuiCreateContentOfTab(player) return end end end -function OarcGuiOnGuiClosedEvent(event) +--@param event EventData.on_gui_closed +function OarcGuiClosed(event) if (event.element and (event.element.name == "oarc_gui")) then HideOarcGui(game.players[event.player_index]) end -end \ No newline at end of file +end diff --git a/lib/oarc_gui_utils.lua b/lib/oarc_gui_utils.lua index 834ef79..4bef959 100644 --- a/lib/oarc_gui_utils.lua +++ b/lib/oarc_gui_utils.lua @@ -1,16 +1,18 @@ --- oarc_gui_utils.lua --- Mar 2019 - -- Generic GUI stuff goes here. +GENERIC_GUI_MAX_HEIGHT = 500 + -------------------------------------------------------------------------------- -- GUI Styles -------------------------------------------------------------------------------- - +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_fixed_width_style = { minimal_width = 450, maximal_width = 450 } +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_label_style = { -- minimal_width = 450, -- maximal_width = 50, @@ -19,6 +21,8 @@ my_label_style = { top_padding = 0, bottom_padding = 0 } +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_label_header_style = { single_line = false, font = "heading-1", @@ -26,6 +30,17 @@ my_label_header_style = { top_padding = 0, bottom_padding = 0 } +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields +my_label_header2_style = { + single_line = false, + font = "heading-2", + font_color = {r=1,g=1,b=1}, + top_padding = 0, + bottom_padding = 0 +} +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_label_header_grey_style = { single_line = false, font = "heading-1", @@ -33,40 +48,55 @@ my_label_header_grey_style = { top_padding = 0, bottom_padding = 0 } +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_note_style = { -- minimal_width = 450, single_line = false, - font = "default-small-semibold", + font = "default-semibold", font_color = {r=1,g=0.5,b=0.5}, top_padding = 0, bottom_padding = 0 } +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_warning_style = { -- minimal_width = 450, -- maximal_width = 450, single_line = false, - font_color = {r=1,g=0.1,b=0.1}, + font = "default-bold", + font_color = {r=1,g=0.3,b=0.3}, top_padding = 0, bottom_padding = 0 } +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_spacer_style = { minimal_height = 10, top_padding = 0, bottom_padding = 0 } +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_small_button_style = { font = "default-small-semibold" } +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_player_list_fixed_width_style = { minimal_width = 200, maximal_width = 400, maximal_height = 200 } +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_shared_item_list_fixed_width_style = { minimal_width = 200, maximal_width = 600, maximal_height = 600 } +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_player_list_admin_style = { font = "default-semibold", font_color = {r=1,g=0.5,b=0.5}, @@ -75,6 +105,8 @@ my_player_list_admin_style = { bottom_padding = 0, single_line = false, } +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_player_list_style = { font = "default-semibold", minimal_width = 200, @@ -82,6 +114,8 @@ my_player_list_style = { bottom_padding = 0, single_line = false, } +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_player_list_offline_style = { -- font = "default-semibold", font_color = {r=0.5,g=0.5,b=0.5}, @@ -90,11 +124,16 @@ my_player_list_offline_style = { bottom_padding = 0, single_line = false, } +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_player_list_style_spacer = { minimal_height = 20, } +---@type Color my_color_red = {r=1,g=0.1,b=0.1} +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_longer_label_style = { maximal_width = 600, single_line = false, @@ -102,6 +141,8 @@ my_longer_label_style = { top_padding = 0, bottom_padding = 0 } +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_longer_warning_style = { maximal_width = 600, single_line = false, @@ -109,6 +150,8 @@ my_longer_warning_style = { top_padding = 0, bottom_padding = 0 } +---@type LuaStyle +---@diagnostic disable-next-line: missing-fields my_notepad_fixed_width_style = { minimal_width = 600, maximal_width = 600, @@ -124,29 +167,42 @@ my_notepad_fixed_width_style = { -- GUI Functions -------------------------------------------------------------------------------- --- Apply a style option to a GUI -function ApplyStyle (guiIn, styleIn) - for k,v in pairs(styleIn) do - guiIn.style[k]=v +---Apply a style option to a GUI +---@param gui_element LuaGuiElement +---@param style_in table +---@return nil +function ApplyStyle (gui_element, style_in) + for k,v in pairs(style_in) do + gui_element.style[k]=v end end --- Shorter way to add a label with a style -function AddLabel(guiIn, name, message, style) - local g = guiIn.add{name = name, type = "label", - caption=message} +---Shorter way to add a label with a style +---@param gui_element LuaGuiElement +---@param name string? +---@param message LocalisedString +---@param style table|string +---@return LuaGuiElement +function AddLabel(gui_element, name, message, style) + local g = gui_element.add{name = name, type = "label", caption=message} if (type(style) == "table") then ApplyStyle(g, style) else g.style = style end + return g end --- Shorter way to add a spacer -function AddSpacer(guiIn) - ApplyStyle(guiIn.add{type = "label", caption=" "}, my_spacer_style) +---Shorter way to add a spacer +---@param gui_element LuaGuiElement +---@return nil +function AddSpacer(gui_element) + ApplyStyle(gui_element.add{type = "label", caption=" "}, my_spacer_style) end -function AddSpacerLine(guiIn) - ApplyStyle(guiIn.add{type = "line", direction="horizontal"}, my_spacer_style) +---Shorter way to add a spacer line +---@param gui_element LuaGuiElement +---@return nil +function AddSpacerLine(gui_element) + ApplyStyle(gui_element.add{type = "line", direction="horizontal"}, my_spacer_style) end \ No newline at end of file diff --git a/lib/oarc_store_map_features.lua b/lib/oarc_store_map_features.lua deleted file mode 100644 index a85a703..0000000 --- a/lib/oarc_store_map_features.lua +++ /dev/null @@ -1,337 +0,0 @@ --- oarc_store_map_features.lua --- May 2020 --- Adding microtransactions. - -require("lib/shared_chests") -require("lib/map_features") -local mod_gui = require("mod-gui") - -OARC_STORE_MAP_TEXT = -{ - special_chests = "Special buildings for sharing or monitoring items and energy. This will convert the closest wooden chest (to you) within 16 tiles into a special building of your choice. Make sure to leave enough space! The combinators and accumulators can take up several tiles around them.", - special_chunks = "Map features that can be built on the special empty chunks found on the map. You must be standing inside an empty special chunk to be able to build these. Each player can only build one of each type. [color=red]THESE FEATURES ARE PERMANENT AND CAN NOT BE REMOVED![/color]", - special_buttons = "Special buttons like teleporting home and placing waterfill.", - reset_buttons = "Reset your player and base. [color=red]Choose carefully! Can't be undone.[/color] If you don't own a base and your own force, some options may not be available to you." -} - --- N = number already purchased --- Cost = initial + (additional * ( N^multiplier )) -OARC_STORE_MAP_FEATURES = -{ - special_chests = { - ["logistic-chest-storage"] = { - initial_cost = 200, - additional_cost = 20, - multiplier_cost = 2, - max_cost = 2000, - -- limit = 100, - text="Input chest for storing shared items."}, - ["logistic-chest-requester"] = { - initial_cost = 200, - additional_cost = 50, - multiplier_cost = 2, - max_cost = 4000, - -- limit = 100, - text="Output chest for requesting shared items."}, - ["constant-combinator"] = { - initial_cost = 50, - text="Combinator setup to monitor shared items."}, - ["accumulator"] = { - initial_cost = 200, - additional_cost = 50, - multiplier_cost = 2, - max_cost = 2000, - -- limit = 100, - text="INPUT for shared energy system. [color=red]Only starts to share once it is charged to 50%.[/color]"}, - ["electric-energy-interface"] = { - initial_cost = 200, - additional_cost = 100, - multiplier_cost = 2, - max_cost = 4000, - -- limit = 100, - text="OUTPUT for shared energy system. [color=red]Will NOT power other special eletric interfaces! You especially can't power special chunks with this![/color]"}, - ["deconstruction-planner"] = { - initial_cost = 0, - text="Removes the closest special building within range. NO REFUNDS!"}, - }, - - special_chunks = { - ["electric-furnace"] = { - initial_cost = 1000, - additional_cost = 1000, - multiplier_cost = 2, - -- limit = 3, - text="Build a special furnace chunk here. Contains 4 furnaces that run at very high speeds. [color=red]Requires energy from the shared storage. Modules have no effect![/color]"}, - ["oil-refinery"] = { - initial_cost = 1000, - additional_cost = 1000, - multiplier_cost = 2, - -- limit = 3, - text="Build a special oil refinery chunk here. Contains 2 refineries and some chemical plants that run at very high speeds. [color=red]Requires energy from the shared storage. Modules have no effect![/color]"}, - ["assembling-machine-3"] = { - initial_cost = 1000, - additional_cost = 1000, - multiplier_cost = 2, - -- limit = 3, - text="Build a special assembly machine chunk here. Contains 6 assembling machines that run at very high speeds. [color=red]Requires energy from the shared storage. Modules have no effect![/color]"}, - ["centrifuge"] = { - initial_cost = 1000, - additional_cost = 1000, - multiplier_cost = 2, - -- limit = 1, - text="Build a special centrifuge chunk here. Contains 1 centrifuge that runs at very high speeds. [color=red]Requires energy from the shared storage. Modules have no effect![/color]"}, - ["rocket-silo"] = { - initial_cost = 1000, - additional_cost = 0, - multiplier_cost = 2, - max_cost = 10000, - -- limit = 2, - text="Convert this special chunk into a rocket launch pad. This allows you to build a rocket silo here!"}, - }, - - -- special_chunks_upgrades = { - -- ["big-electric-pole"] = { - -- cost = 0, - -- text = "Upgrade your special chunk so that it pulls power from the cloud! Refills the accumulator from the cloud automatically if it falls below 50%." - -- } - - -- } - - special_buttons = { - ["assembling-machine-1"] = { - initial_cost = 10, - text="Teleport home."}, - ["offshore-pump"] = { - initial_cost = 50, - text="Converts the closest empty wooden chest into a water tile!" - } - }, - - reset_buttons = { - ["electronic-circuit"] = { - initial_cost = 5000, - solo_force = true, - text="DESTROY your base and restart. This allows you to choose a new spawn and will completely destroy all your buildings and your force. All technology progress will be reset. You get to keep your current items and armor! [color=red]THERE IS NO CONFIRMATION PROMPT! THIS CAN NOT BE UNDONE![/color]" - }, - ["advanced-circuit"] = { - initial_cost = 5000, - solo_force = true, - text="ABANDON your base and restart. This allows you to choose a new spawn and will move all your buildings to a neutral force. They will still be on the map and can be interacted with, but will not be owned by any player or player force. All radars will be destroyed to help trim map size. You get to keep your current items and armor! [color=red]THERE IS NO CONFIRMATION PROMPT! THIS CAN NOT BE UNDONE![/color]" - }, - ["processing-unit"] = { - initial_cost = 5000, - text="Restart your game. This will reset your player, your force and your base. [color=red]THERE IS NO CONFIRMATION PROMPT! THIS CAN NOT BE UNDONE![/color]" - } - } -} - -function CreateMapFeatureStoreTab(tab_container, player) - - local player_inv = player.get_main_inventory() - if (player_inv == nil) then return end - - local wallet = player_inv.get_item_count("coin") - AddLabel(tab_container, - "map_feature_store_wallet_lbl", - "Coins Available: " .. wallet .. " [item=coin]", - {top_margin=5, bottom_margin=5}) - AddLabel(tab_container, "coin_info", "Players start with some coins. Earn more coins by killing enemies.", my_note_style) - - local line = tab_container.add{type="line", direction="horizontal"} - line.style.top_margin = 5 - line.style.bottom_margin = 5 - - for category,section in pairs(OARC_STORE_MAP_FEATURES) do - - if (not global.ocfg.enable_chest_sharing and (category == "special_chests")) then - goto SKIP_CATEGORY - end - - if (not global.ocfg.enable_magic_factories and (category == "special_chunks")) then - goto SKIP_CATEGORY - end - - AddLabel(tab_container, - nil, - OARC_STORE_MAP_TEXT[category], - {bottom_margin=5, maximal_width = 400, single_line = false}) - local flow = tab_container.add{name = category, type="flow", direction="horizontal"} - for item_name,item in pairs(section) do - - local blocked = false - if (item.solo_force and ((player.force.name == global.ocfg.main_force) or - (not global.ocore.playerSpawns[player.name]))) then - blocked = true - end - - local count = OarcMapFeaturePlayerCountGet(player, category, item_name) - local cost = OarcMapFeatureCostScaling(player, category, item_name) - local color = "[color=green]" - if ((cost > wallet) or (cost < 0) or blocked) then - color = "[color=red]" - end - local btn = flow.add{name=item_name, - type="sprite-button", - -- number=item.count, - sprite="item/"..item_name, - -- tooltip=item.text.." Cost: "..color..cost.."[/color] [item=coin]", - style=mod_gui.button_style} - if (cost < 0) then - btn.enabled = false - btn.tooltip = item.text .. "\n "..color.. - "Limit: ("..count.."/"..item.limit..") [/color]" - elseif (blocked) then - btn.enabled = false - btn.tooltip = item.text .. " (This is only allowed for players on their own force that own the spawn. If you have other players on your force, they must reset first before you can use this.)" .." Cost: "..color..cost.."[/color] [item=coin]" - elseif (item.limit) then - btn.tooltip = item.text .. "\nCost: "..color..cost.."[/color] [item=coin] ".. - "Limit: ("..count.."/"..item.limit..")" - else - btn.tooltip = item.text.." Cost: "..color..cost.."[/color] [item=coin]" - end - - end - - -- Spacer - local line2 = tab_container.add{type="line", direction="horizontal"} - line2.style.top_margin = 5 - line2.style.bottom_margin = 5 - - ::SKIP_CATEGORY:: - end -end - -function OarcMapFeatureInitGlobalCounters() - global.oarc_store = {} - global.oarc_store.pmf_counts = {} -end - -function OarcMapFeaturePlayerCreatedEvent(player) - global.oarc_store.pmf_counts[player.name] = {} -end - -function OarcMapFeaturePlayerCountGet(player, category_name, feature_name) - if (not global.oarc_store.pmf_counts[player.name][feature_name]) then - global.oarc_store.pmf_counts[player.name][feature_name] = 0 - return 0 - end - - return global.oarc_store.pmf_counts[player.name][feature_name] -end - -function OarcMapFeaturePlayerCountChange(player, category_name, feature_name, change) - - if (not global.oarc_store.pmf_counts[player.name][feature_name]) then - if (change < 0) then - log("ERROR - OarcMapFeaturePlayerCountChange - Removing when count is not set??") - end - global.oarc_store.pmf_counts[player.name][feature_name] = change - return - end - - -- Update count - global.oarc_store.pmf_counts[player.name][feature_name] = global.oarc_store.pmf_counts[player.name][feature_name] + change - - -- Make sure we don't go below 0. - if (global.oarc_store.pmf_counts[player.name][feature_name] < 0) then - global.oarc_store.pmf_counts[player.name][feature_name] = 0 - end -end - - - --- Return cost (0 or more) or return -1 if disabled. -function OarcMapFeatureCostScaling(player, category_name, feature_name) - - local map_feature = OARC_STORE_MAP_FEATURES[category_name][feature_name] - - -- Check limit first. - local count = OarcMapFeaturePlayerCountGet(player, category_name, feature_name) - if (map_feature.limit and (count >= map_feature.limit)) then - return -1 - end - - if (map_feature.initial_cost and map_feature.additional_cost and map_feature.multiplier_cost) then - local calc_cost = (map_feature.initial_cost + (map_feature.additional_cost*(count^map_feature.multiplier_cost))) - if (map_feature.max_cost) then - return math.min(map_feature.max_cost, calc_cost) - else - return calc_cost - end - else - return map_feature.initial_cost - end -end - -function OarcMapFeatureStoreButton(event) - local button = event.element - local player = game.players[event.player_index] - - local player_inv = player.get_inventory(defines.inventory.character_main) - if (player_inv == nil) then return end - local wallet = player_inv.get_item_count("coin") - - local map_feature = OARC_STORE_MAP_FEATURES[button.parent.name][button.name] - - -- Calculate cost based on how many player has purchased? - local cost = OarcMapFeatureCostScaling(player, button.parent.name, button.name) - - -- Check if we have enough money - if (wallet < cost) then - player.print("You're broke! Go kill some enemies or beg for change...") - return - end - - if (player.vehicle) then - player.print("Sir, please step out of the vehicle before you try to make any purchases...") - return - end - - -- Each button has a special function - local result = false - if (button.name == "logistic-chest-storage") then - result = ConvertWoodenChestToSharedChestInput(player) - elseif (button.name == "logistic-chest-requester") then - result = ConvertWoodenChestToSharedChestOutput(player) - elseif (button.name == "constant-combinator") then - result = ConvertWoodenChestToSharedChestCombinators(player) - elseif (button.name == "accumulator") then - result = ConvertWoodenChestToShareEnergyInput(player) - elseif (button.name == "electric-energy-interface") then - result = ConvertWoodenChestToShareEnergyOutput(player) - elseif (button.name == "deconstruction-planner") then - result = DestroyClosestSharedChestEntity(player) - elseif (button.name == "electric-furnace") then - result = RequestSpawnSpecialChunk(player, SpawnFurnaceChunk, button.name) - elseif (button.name == "oil-refinery") then - result = RequestSpawnSpecialChunk(player, SpawnOilRefineryChunk, button.name) - elseif (button.name == "assembling-machine-3") then - result = RequestSpawnSpecialChunk(player, SpawnAssemblyChunk, button.name) - elseif (button.name == "centrifuge") then - result = RequestSpawnSpecialChunk(player, SpawnCentrifugeChunk, button.name) - elseif (button.name == "rocket-silo") then - result = RequestSpawnSpecialChunk(player, SpawnSiloChunk, button.name) - elseif (button.name == "assembling-machine-1") then - SendPlayerToSpawn(player) - result = true - elseif (button.name == "offshore-pump") then - result = ConvertWoodenChestToWaterFill(player) - elseif (button.name == "electronic-circuit") then - ResetPlayerAndDestroyForce(player) - result = true - elseif (button.name == "advanced-circuit") then - ResetPlayerAndAbandonForce(player) - result = true - elseif (button.name == "processing-unit") then - ResetPlayerAndMergeForceToNeutral(player) - result = true - end - - -- On success, we deduct money - if (result) then - player_inv.remove({name = "coin", count = cost}) - end - - -- Refresh GUI: - FakeTabChangeEventOarcStore(player) -end diff --git a/lib/oarc_store_player_items.lua b/lib/oarc_store_player_items.lua deleted file mode 100644 index fba4451..0000000 --- a/lib/oarc_store_player_items.lua +++ /dev/null @@ -1,160 +0,0 @@ --- oarc_store_player_items.lua --- May 2020 --- Adding microtransactions. - -local mod_gui = require("mod-gui") - -OARC_STORE_PLAYER_ITEMS = -{ - ["Guns"] = { - ["pistol"] = {cost = 1, count = 1, play_time_locked=false}, - ["shotgun"] = {cost = 5, count = 1, play_time_locked=false}, - ["submachine-gun"] = {cost = 10, count = 1, play_time_locked=false}, - ["flamethrower"] = {cost = 50, count = 1, play_time_locked=true}, - ["rocket-launcher"] = {cost = 50, count = 1, play_time_locked=true}, - -- ["railgun"] = {cost = 250, count = 1, play_time_locked=true}, -- SAD - }, - - ["Turrets"] = { - ["gun-turret"] = {cost = 25, count = 1, play_time_locked=false}, - ["flamethrower-turret"] = {cost = 50, count = 1, play_time_locked=false}, - ["laser-turret"] = {cost = 75, count = 1, play_time_locked=false}, - ["artillery-turret"] = {cost = 500, count = 1, play_time_locked=true}, - }, - - ["Ammo"] = { - ["firearm-magazine"] = {cost = 10, count = 10, play_time_locked=false}, - ["piercing-rounds-magazine"] = {cost = 30, count = 10, play_time_locked=false}, - ["shotgun-shell"] = {cost = 10, count = 10, play_time_locked=false}, - ["flamethrower-ammo"] = {cost = 50, count = 10, play_time_locked=true}, - ["rocket"] = {cost = 100, count = 10, play_time_locked=true}, - -- ["railgun-dart"] = {cost = 250, count = 10, play_time_locked=true}, -- SAD - ["atomic-bomb"] = {cost = 1000, count = 1, play_time_locked=true}, - ["artillery-shell"] = {cost = 50, count = 1, play_time_locked=true}, - - }, - - ["Special"] = { - ["repair-pack"] = {cost = 1, count = 1, play_time_locked=false}, - ["raw-fish"] = {cost = 1, count = 1, play_time_locked=false}, - ["grenade"] = {cost = 20, count = 10, play_time_locked=true}, - ["cliff-explosives"] = {cost = 20, count = 10, play_time_locked=true}, - ["artillery-targeting-remote"] = {cost = 500, count = 1, play_time_locked=true}, - }, - - ["Capsules/Mines"] = { - ["land-mine"] = {cost = 20, count = 10, play_time_locked=false}, - ["defender-capsule"] = {cost = 20, count = 10, play_time_locked=false}, - ["distractor-capsule"] = {cost = 40, count = 10, play_time_locked=false}, - ["destroyer-capsule"] = {cost = 60, count = 10, play_time_locked=false}, - ["poison-capsule"] = {cost = 50, count = 10, play_time_locked=false}, - ["slowdown-capsule"] = {cost = 25, count = 10, play_time_locked=false}, - }, - - ["Armor"] = { - ["light-armor"] = {cost = 10, count = 1, play_time_locked=false}, - ["heavy-armor"] = {cost = 20, count = 1, play_time_locked=false}, - ["modular-armor"] = {cost = 200, count = 1, play_time_locked=false}, - ["power-armor"] = {cost = 1000, count = 1, play_time_locked=false}, - ["power-armor-mk2"] = {cost = 5000, count = 1, play_time_locked=false}, - }, - - ["Power Equipment"] = { - ["fusion-reactor-equipment"] = {cost = 1000, count = 1, play_time_locked=false}, - ["battery-equipment"] = {cost = 100, count = 1, play_time_locked=false}, - ["battery-mk2-equipment"] = {cost = 1000, count = 1, play_time_locked=false}, - ["solar-panel-equipment"] = {cost = 10, count = 1, play_time_locked=false}, - }, - - ["Bot Equipment"] = { - ["personal-roboport-equipment"] = {cost = 100, count = 1, play_time_locked=false}, - ["personal-roboport-mk2-equipment"] = {cost = 500, count = 1, play_time_locked=false}, - ["construction-robot"] = {cost = 100, count = 10, play_time_locked=false}, - ["roboport"] = {cost = 1000, count = 1, play_time_locked=false}, - ["logistic-chest-storage"] = {cost = 100, count = 1, play_time_locked=false}, - }, - - ["Misc Equipment"] = { - ["belt-immunity-equipment"] = {cost = 10, count = 1, play_time_locked=false}, - ["exoskeleton-equipment"] = {cost = 100, count = 1, play_time_locked=false}, - ["night-vision-equipment"] = {cost = 50, count = 1, play_time_locked=false}, - - ["personal-laser-defense-equipment"] = {cost = 100, count = 1, play_time_locked=false}, - -- ["discharge-defense-equipment"] = {cost = 1, count = 1, play_time_locked=false}, - ["energy-shield-equipment"] = {cost = 50, count = 1, play_time_locked=false}, - ["energy-shield-mk2-equipment"] = {cost = 500, count = 1, play_time_locked=false}, - }, - - ["Spidertron"] = { - ["spidertron"] = {cost = 5000, count = 1, play_time_locked=false}, - ["spidertron-remote"] = {cost = 500, count = 1, play_time_locked=false}, - }, -} - -function CreatePlayerStoreTab(tab_container, player) - - local player_inv = player.get_main_inventory() - if (player_inv == nil) then return end - - local wallet = player_inv.get_item_count("coin") - AddLabel(tab_container, - "player_store_wallet_lbl", - "Coins Available: " .. wallet .. " [item=coin]", - {top_margin=5, bottom_margin=5}) - AddLabel(tab_container, "coin_info", "Players start with some coins. Earn more coins by killing enemies.", my_note_style) - AddLabel(tab_container, - "player_store_note_lbl", - "Locked items become available after playing for awhile...", - my_note_style) - - local line = tab_container.add{type="line", direction="horizontal"} - line.style.top_margin = 5 - line.style.bottom_margin = 5 - - for category,section in pairs(OARC_STORE_PLAYER_ITEMS) do - local flow = tab_container.add{name = category, type="flow", direction="horizontal"} - for item_name,item in pairs(section) do - local color = "[color=green]" - if (item.cost > wallet) then - color = "[color=red]" - end - local btn = flow.add{name=item_name, - type="sprite-button", - number=item.count, - sprite="item/"..item_name, - tooltip=item_name .. " Cost: "..color..item.cost.."[/color] [item=coin]", - style=mod_gui.button_style} - if (item.play_time_locked and (player.online_time < TICKS_PER_MINUTE*15)) then - btn.enabled = false - end - end - local line2 = tab_container.add{type="line", direction="horizontal"} - line2.style.top_margin = 5 - line2.style.bottom_margin = 5 - end -end - -function OarcPlayerStoreButton(event) - local button = event.element - local player = game.players[event.player_index] - - local player_inv = player.get_inventory(defines.inventory.character_main) - if (player_inv == nil) then return end - - local category = button.parent.name - - local item = OARC_STORE_PLAYER_ITEMS[category][button.name] - - if (player_inv.get_item_count("coin") >= item.cost) then - player_inv.insert({name = button.name, count = item.count}) - player_inv.remove({name = "coin", count = item.cost}) - - if (button.parent and button.parent.parent and button.parent.parent.player_store_wallet_lbl) then - local wallet = player_inv.get_item_count("coin") - button.parent.parent.player_store_wallet_lbl.caption = "Coins Available: " .. wallet .. " [item=coin]" - end - - else - player.print("You're broke! Go kill some enemies or beg for change...") - end -end \ No newline at end of file diff --git a/lib/oarc_tests.lua b/lib/oarc_tests.lua new file mode 100644 index 0000000..a81351c --- /dev/null +++ b/lib/oarc_tests.lua @@ -0,0 +1,214 @@ +local mod_gui = require("mod-gui") + +---Test out all the fonts available in the game. +---@param player LuaPlayer +---@return nil +function TestFonts(player) + local font_list = { + "compi", + "compilatron-message-font", + "count-font", + "default", + "default-bold", + "default-dialog-button", + "default-dropdown", + "default-game", + "default-large", + "default-large-bold", + "default-large-semibold", + "default-listbox", + "default-semibold", + "default-small", + "default-small-bold", + "default-small-semibold", + "default-tiny-bold", + "heading-1", + "heading-2", + "heading-3", + "locale-pick", + "scenario-message-dialog", + "technology-slot-level-font", + "var", + } + + local test_frame = player.gui.screen.add{type="frame", name="font_test_frame", direction="vertical"} + for _,font in pairs(font_list) do + local test_text = test_frame.add{type="label", caption=font} + test_text.style.font = font + end + + test_frame.auto_center = true +end + +function ClearTestFonts(player) + if player.gui.screen.font_test_frame then + player.gui.screen.font_test_frame.destroy() + end +end + +---Test out all the button styles available in the game. +---@param player LuaPlayer +---@return nil +function TestButtons(player) + local button_styles = { + "back_button", + "big_slot_button", + -- "blueprint_drop_slot_button", + "blueprint_record_selection_button", + "blueprint_record_slot_button", + "browse_games_gui_toggle_favorite_off_button", + "browse_games_gui_toggle_favorite_on_button", + "cancel_close_button", + -- "character_gui_entity_button", + "choose_chat_icon_button", + "choose_chat_icon_in_textbox_button", + "close_button", + "compact_slot_sized_button", + "confirm_button", + "confirm_double_arrow_button", + "confirm_in_load_game_button", + "control_settings_button", + "control_settings_section_button", + "current_research_info_button", + "dark_button", + "dark_rounded_button", + "dialog_button", + "drop_target_button", + "dropdown_button", + "entity_variation_button", + "forward_button", + "frame_action_button", + "frame_button", + "green_button", + "highlighted_tool_button", + "inventory_limit_slot_button", + "left_slider_button", + "locomotive_minimap_button", + "logistic_slot_button", + "map_generator_close_preview_button", + "map_generator_confirm_button", + "map_generator_preview_button", + "map_view_add_button", + "map_view_options_button", + "menu_button", + "mini_button", + "mod_gui_button", + "not_working_weapon_button", + "open_armor_button", + "other_settings_gui_button", + "quick_bar_page_button", + "quick_bar_slot_button", + "recipe_slot_button", + "red_back_button", + "red_button", + "red_confirm_button", + "red_logistic_slot_button", + "red_slot_button", + "research_queue_cancel_button", + "right_slider_button", + "rounded_button", + "shortcut_bar_button", + "shortcut_bar_expand_button", + "side_menu_button", + "slider_button", + "slot_button", + "slot_sized_button", + "station_train_status_button", + "statistics_slot_button", + "tile_variation_button", + "tip_notice_button", + "tip_notice_close_button", + "tool_bar_open_button", + "tool_button", + "tracking_off_button", + "tracking_on_button", + "train_schedule_action_button", + "train_schedule_add_station_button", + "train_schedule_add_wait_condition_button", + "train_schedule_comparison_type_button", + "train_schedule_condition_time_selection_button", + "train_schedule_delete_button", + "train_schedule_fulfilled_delete_button", + "train_schedule_fulfilled_item_select_button", + "train_schedule_item_select_button", + "train_schedule_temporary_station_delete_button", + "train_status_button", + -- "train_stop_entity_button", + -- "wide_entity_button", + "working_weapon_button", + "yellow_logistic_slot_button", + } + + local test_frame = player.gui.screen.add{type="scroll-pane", name="button_test_frame", direction="vertical"} + -- test_frame.auto_center = true + test_frame.vertical_scroll_policy = "auto" + test_frame.style.maximal_height = 800 + + for _,button_style in pairs(button_styles) do + local test_button = test_frame.add + { + type="button", + caption=button_style, + style=button_style + } + end + + +end + +function ClearTestButtons(player) + if player.gui.screen.button_test_frame then + player.gui.screen.button_test_frame.destroy() + end +end + +function RecreateOarcGui(player) + if (mod_gui.get_button_flow(player).oarc_button ~= nil) then + mod_gui.get_button_flow(player).oarc_button.destroy() + end + + if (mod_gui.get_frame_flow(player)[OARC_GUI] ~= nil) then + mod_gui.get_frame_flow(player)[OARC_GUI].destroy() + end + + InitOarcGuiTabs(player) +end + + +function SetNauvisChunksGenerated() + local nauvis = game.surfaces["nauvis"] + + for x = -100, 100, 1 do + for y = -100, 100, 1 do + nauvis.set_chunk_generated_status({x=x, y=y}, defines.chunk_generated_status.entities) + end + end +end + + +function FlagEnemyForce(player, enemy_force_name) + + local enemy_force = game.forces[enemy_force_name] + + player.force.set_friend(enemy_force, true) + player.force.set_cease_fire(enemy_force, true) + +end + +function UnflagEnemyForce(player, enemy_force_name) + + local enemy_force = game.forces[enemy_force_name] + + player.force.set_friend(enemy_force, false) + player.force.set_cease_fire(enemy_force, false) + +end + +function CreateTestSurfaces() + + game.create_surface("vulcanus") + game.create_surface("fulgora") + game.create_surface("gleba") + game.create_surface("aquilo") + +end \ No newline at end of file diff --git a/lib/oarc_utils.lua b/lib/oarc_utils.lua index 00dc83c..0087f13 100644 --- a/lib/oarc_utils.lua +++ b/lib/oarc_utils.lua @@ -1,12 +1,11 @@ --- oarc_utils.lua --- Nov 2016 --- --- My general purpose utility functions for factorio --- Also contains some constants and gui styles +-- My general purpose utility functions and constants for factorio +-- Also contains some constants require("lib/oarc_gui_utils") require("mod-gui") +local util = require("util") + -------------------------------------------------------------------------------- -- Useful constants -------------------------------------------------------------------------------- @@ -22,101 +21,112 @@ MAX_INT32_NEG = -2147483648 --------------------------------------------------------------------------------- --- General Helper Functions --------------------------------------------------------------------------------- +-- -------------------------------------------------------------------------------- +-- -- General Helper Functions +-- -------------------------------------------------------------------------------- ---- Tests if a string contains a given substring --- @param s the string to check for the substring --- @param ends the substring to test for --- @return true if the substring was found in the string -function string.contains(s, ends) - return s and string.find(s, ends) ~= nil -end - --- Prints flying text. --- Color is optional -function FlyingText(msg, pos, color, surface) - if color == nil then - surface.create_entity({ name = "flying-text", position = pos, text = msg }) - else - surface.create_entity({ name = "flying-text", position = pos, text = msg, color = color }) - end -end +-- -- Prints flying text. +-- -- Color is optional +-- function FlyingText(msg, pos, color, surface) +-- if color == nil then +-- surface.create_entity({ name = "flying-text", position = pos, text = msg }) +-- else +-- surface.create_entity({ name = "flying-text", position = pos, text = msg, color = color }) +-- end +-- end -- Get a printable GPS string -function GetGPStext(pos) - return "[gps=" .. pos.x .. "," .. pos.y .. "]" -end - --- Requires having an on_tick handler. -function DisplaySpeechBubble(player, text, timeout_secs) - - if (global.oarc_speech_bubbles == nil) then - global.oarc_speech_bubbles = {} - end - - if (player and player.character) then - local sp = player.surface.create_entity{name = "compi-speech-bubble", - position = player.position, - text = text, - source = player.character} - table.insert(global.oarc_speech_bubbles, {entity=sp, - timeout_tick=game.tick+(timeout_secs*TICKS_PER_SECOND)}) - end -end - --- Render some text on the ground. Visible to all players. Forever. -function RenderPermanentGroundText(surface, position, scale, text, color) - rendering.draw_text{text=text, - surface=surface, - target=position, - color=color, - scale=scale, - --Allowed fonts: default-dialog-button default-game compilatron-message-font default-large default-large-semibold default-large-bold heading-1 compi - font="compi", - draw_on_ground=true} -end - --- A standardized helper text that fades out over time -function TemporaryHelperText(text, position, ttl) - local rid = rendering.draw_text{text=text, - surface=game.surfaces[GAME_SURFACE_NAME], - target=position, - color={0.7,0.7,0.7,0.7}, - scale=1, - font="compi", - time_to_live=ttl, - draw_on_ground=false} +---@param surface_name string +---@param position MapPosition +---@return string +function GetGPStext(surface_name, position) + return "[gps=" .. position.x .. "," .. position.y .. "," .. surface_name .. "]" +end + +-- -- Requires having an on_tick handler. +-- function DisplaySpeechBubble(player, text, timeout_secs) + +-- if (global.oarc_speech_bubbles == nil) then +-- global.oarc_speech_bubbles = {} +-- end + +-- if (player and player.character) then +-- local sp = player.surface.create_entity{name = "compi-speech-bubble", +-- position = player.position, +-- text = text, +-- source = player.character} +-- table.insert(global.oarc_speech_bubbles, {entity=sp, +-- timeout_tick=game.tick+(timeout_secs*TICKS_PER_SECOND)}) +-- end +-- end + +---Render some text on the ground. Visible to all players. Forever. +---@param surface LuaSurface +---@param position MapPosition +---@param scale number +---@param text string +---@param color Color +---@param alignment TextAlign? +---@return nil +function RenderPermanentGroundText(surface, position, scale, text, color, alignment) + rendering.draw_text { text = text, + surface = surface, + target = position, + color = color, + scale = scale, + --Allowed fonts: default-dialog-button default-game compilatron-message-font default-large default-large-semibold default-large-bold heading-1 compi + font = "compi", + alignment = alignment, + draw_on_ground = true } +end + +---A standardized helper text that fades out over time +---@param text string|LocalisedString +---@param surface LuaSurface +---@param position MapPosition +---@param ttl number +---@param alignment TextAlign? +---@return nil +function TemporaryHelperText(text, surface, position, ttl, alignment) + local rid = rendering.draw_text { text = text, + surface = surface, + target = position, + color = { 0.7, 0.7, 0.7, 0.7 }, + scale = 1, + font = "compi", + time_to_live = ttl, + alignment = alignment, + draw_on_ground = false } table.insert(global.oarc_renders_fadeout, rid) end --- Every second, check a global table to see if we have any speech bubbles to kill. -function TimeoutSpeechBubblesOnTick() - if ((game.tick % (TICKS_PER_SECOND)) == 3) then - if (global.oarc_speech_bubbles and (#global.oarc_speech_bubbles > 0)) then - for k,sp in pairs(global.oarc_speech_bubbles) do - if (game.tick > sp.timeout_tick) then - if (sp.entity ~= nil) and (sp.entity.valid) then - sp.entity.start_fading_out() - end - table.remove(global.oarc_speech_bubbles, k) - end - end - end - end -end - --- Every tick, check a global table to see if we have any rendered thing that needs fading out. +-- -- Every second, check a global table to see if we have any speech bubbles to kill. +-- function TimeoutSpeechBubblesOnTick() +-- if ((game.tick % (TICKS_PER_SECOND)) == 3) then +-- if (global.oarc_speech_bubbles and (#global.oarc_speech_bubbles > 0)) then +-- for k,sp in pairs(global.oarc_speech_bubbles) do +-- if (game.tick > sp.timeout_tick) then +-- if (sp.entity ~= nil) and (sp.entity.valid) then +-- sp.entity.start_fading_out() +-- end +-- table.remove(global.oarc_speech_bubbles, k) +-- end +-- end +-- end +-- end +-- end + +---Every tick, check a global table to see if we have any rendered thing that needs fading out. +---@return nil function FadeoutRenderOnTick() if (global.oarc_renders_fadeout and (#global.oarc_renders_fadeout > 0)) then - for k,rid in pairs(global.oarc_renders_fadeout) do + for k, rid in pairs(global.oarc_renders_fadeout) do if (rendering.is_valid(rid)) then local ttl = rendering.get_time_to_live(rid) if ((ttl > 0) and (ttl < 200)) then local color = rendering.get_color(rid) if (color.a > 0.005) then - rendering.set_color(rid, {r=color.r, g=color.g, b=color.b, a=color.a-0.005}) + rendering.set_color(rid, { r = color.r, g = color.g, b = color.b, a = color.a - 0.005 }) end end else @@ -126,38 +136,77 @@ function FadeoutRenderOnTick() end end --- Broadcast messages to all connected players +--- Broadcast messages to all connected players +---@param msg LocalisedString +---@return nil function SendBroadcastMsg(msg) - for name,player in pairs(game.connected_players) do + for name, player in pairs(game.connected_players) do player.print(msg) end end --- Send a message to a player, safely checks if they exist and are online. +---Send a message to a player, safely checks if they exist and are online. +---@param playerName string +---@param msg LocalisedString +---@return nil function SendMsg(playerName, msg) if ((game.players[playerName] ~= nil) and (game.players[playerName].connected)) then game.players[playerName].print(msg) end end --- Simple way to write to a file. Always appends. Only server. --- Has a global setting for enable/disable -function ServerWriteFile(filename, msg) - if (global.ocfg.enable_server_write_files) then - game.write_file(filename, msg, true, 0) +---Checks if a string starts with another string +---@param string string The string to check +---@param start string The starting string to look for +function StringStartsWith(string, start) + return string:sub(1, #start) == start +end + +---Checks if a surface is blacklisted based on the global.ocfg settings +---@param surface_name string +---@return boolean --true if blacklisted +function IsSurfaceBlacklisted(surface_name) + if (global.ocfg.surfaces_blacklist == nil) then + for _,name in pairs(global.ocfg.surfaces_blacklist) do + if (name == surface_name) then + return true + end + end + end + + if (global.ocfg.surfaces_blacklist_match == nil) then + for _,match in pairs(global.ocfg.surfaces_blacklist_match) do + if (StringStartsWith(surface_name, match)) then + return true + end + end end + + return false end --- Useful for displaying game time in mins:secs format -function formattime(ticks) +-- -- Simple way to write to a file. Always appends. Only server. +-- -- Has a global setting for enable/disable +-- function ServerWriteFile(filename, msg) +-- if (global.ocfg.enable_server_write_files) then +-- game.write_file(filename, msg, true, 0) +-- end +-- end + +---Useful for displaying game time in mins:secs format +---@param ticks number +---@return string +function FormatTime(ticks) local seconds = ticks / 60 local minutes = math.floor((seconds)/60) local seconds = math.floor(seconds - 60*minutes) return string.format("%dm:%02ds", minutes, seconds) end --- Useful for displaying game time in mins:secs format -function formattime_hours_mins(ticks) +---Useful for displaying game time in hrs:mins format +---@param ticks number +---@return string +function FormatTimeHoursSecs(ticks) local seconds = ticks / 60 local minutes = math.floor((seconds)/60) local hours = math.floor((minutes)/60) @@ -165,197 +214,268 @@ function formattime_hours_mins(ticks) return string.format("%dh:%02dm", hours, minutes) end --- Simple math clamp -function clamp(val, min, max) - if (val > max) then - return max - elseif (val < min) then - return min - end - return val -end -function clampInt32(val) - return clamp(val, MAX_INT32_NEG, MAX_INT32_POS) -end - -function MathRound(num) - return math.floor(num+0.5) -end +-- -- Simple math clamp +-- function clamp(val, min, max) +-- if (val > max) then +-- return max +-- elseif (val < min) then +-- return min +-- end +-- return val +-- end +-- function clampInt32(val) +-- return clamp(val, MAX_INT32_NEG, MAX_INT32_POS) +-- end --- Simple function to get total number of items in table -function TableLength(T) - local count = 0 - for _ in pairs(T) do count = count + 1 end - return count -end +-- function MathRound(num) +-- return math.floor(num+0.5) +-- end -- Fisher-Yares shuffle -- https://stackoverflow.com/questions/35572435/how-do-you-do-the-fisher-yates-shuffle-in-lua -function FYShuffle(tInput) +---@param T table +---@return table +function FYShuffle(T) local tReturn = {} - for i = #tInput, 1, -1 do + for i = #T, 1, -1 do local j = math.random(i) - tInput[i], tInput[j] = tInput[j], tInput[i] - table.insert(tReturn, tInput[i]) + T[i], T[j] = T[j], T[i] + table.insert(tReturn, T[i]) end return tReturn end --- Get a random KEY from a table. -function GetRandomKeyFromTable(t) - local keyset = {} - for k,v in pairs(t) do - table.insert(keyset, k) - end - return keyset[math.random(#keyset)] -end - --- A safer way to attempt to get the next key in a table. CHECK TABLE SIZE BEFORE CALLING THIS! --- Ensures the key points to a valid entry before calling next. Otherwise it restarts. --- If you get nil as a return, it means you hit the return. -function NextButChecksKeyIsValidFirst(table_in, key) - -- if (table_size(table_in) == 0) then you're fucked end - if ((not key) or (not table_in[key])) then - return next(table_in, nil) - else - return next(table_in, key) +---Check if a table contains a value +---@param table table +---@param val any +---@return boolean +function TableContains(table, val) + for _, value in pairs(table) do + if value == val then + return true + end end + return false end --- Gets the next key, even if we have to start again. -function NextKeyInTableIncludingRestart(table_in, key) - local next_key = NextButChecksKeyIsValidFirst(table_in, key) - if (not next_key) then - return NextButChecksKeyIsValidFirst(table_in, next_key) - else - return next_key +---Get a key from a table given a value (if it exists) +---@param table table +---@param val any +---@return any +function GetTableKey(table, val) + for k, v in pairs(table) do + if v == val then + return k + end end + return nil end -function GetRandomValueFromTable(t) - return t[GetRandomKeyFromTable(t)] -end - --- Simple function to get distance between two positions. -function getDistance(posA, posB) - -- Get the length for each of the components x and y - local xDist = posB.x - posA.x - local yDist = posB.y - posA.y +-- ---Remove a value from a table +-- ---@param table table +-- ---@param val any +-- ---@return nil +-- function TableRemove(t, val) +-- for i = #t, 1, -1 do +-- if t[i] == val then +-- table.remove(t, i) +-- end +-- end +-- end - return math.sqrt( (xDist ^ 2) + (yDist ^ 2) ) -end - --- Given a table of positions, returns key for closest to given pos. -function GetClosestPosFromTable(pos, pos_table) - - local closest_dist = nil - local closest_key = nil - - for k,p in pairs(pos_table) do - local new_dist = getDistance(pos, p) - if (closest_dist == nil) then - closest_dist = new_dist - closest_key = k - elseif (closest_dist > new_dist) then - closest_dist = new_dist - closest_key = k +function TableRemoveOneUsingPairs(t, val) + for k,v in pairs(t) do + if v == val then + table.remove(t, k) + return end end - - if (closest_key == nil) then - log("GetClosestPosFromTable ERROR - None found?") - return nil - end - - return pos_table[closest_key] end +-- -- Get a random KEY from a table. +-- function GetRandomKeyFromTable(t) +-- local keyset = {} +-- for k,v in pairs(t) do +-- table.insert(keyset, k) +-- end +-- return keyset[math.random(#keyset)] +-- end + +-- -- A safer way to attempt to get the next key in a table. CHECK TABLE SIZE BEFORE CALLING THIS! +-- -- Ensures the key points to a valid entry before calling next. Otherwise it restarts. +-- -- If you get nil as a return, it means you hit the return. +-- function NextButChecksKeyIsValidFirst(table_in, key) +-- -- if (table_size(table_in) == 0) then you're fucked end +-- if ((not key) or (not table_in[key])) then +-- return next(table_in, nil) +-- else +-- return next(table_in, key) +-- end +-- end + +-- -- Gets the next key, even if we have to start again. +-- function NextKeyInTableIncludingRestart(table_in, key) +-- local next_key = NextButChecksKeyIsValidFirst(table_in, key) +-- if (not next_key) then +-- return NextButChecksKeyIsValidFirst(table_in, next_key) +-- else +-- return next_key +-- end +-- end + +-- function GetRandomValueFromTable(t) +-- return t[GetRandomKeyFromTable(t)] +-- end + +-- -- Given a table of positions, returns key for closest to given pos. +-- function GetClosestPosFromTable(pos, pos_table) + +-- local closest_dist = nil +-- local closest_key = nil + +-- for k,p in pairs(pos_table) do +-- local new_dist = util.distance(pos, p) +-- if (closest_dist == nil) then +-- closest_dist = new_dist +-- closest_key = k +-- elseif (closest_dist > new_dist) then +-- closest_dist = new_dist +-- closest_key = k +-- end +-- end + +-- if (closest_key == nil) then +-- log("GetClosestPosFromTable ERROR - None found?") +-- return nil +-- end + +-- return pos_table[closest_key] +-- end + -- Chart area for a force +---@param force string|integer|LuaForce +---@param position MapPosition +---@param chunkDist number +---@param surface LuaSurface|string|integer function ChartArea(force, position, chunkDist, surface) force.chart(surface, - {{position.x-(CHUNK_SIZE*chunkDist), - position.y-(CHUNK_SIZE*chunkDist)}, - {position.x+(CHUNK_SIZE*chunkDist), - position.y+(CHUNK_SIZE*chunkDist)}}) -end - --- Give player these default items. -function GivePlayerItems(player) - for name,count in pairs(PLAYER_RESPAWN_START_ITEMS) do - player.insert({name=name, count=count}) - end -end - --- Starter only items -function GivePlayerStarterItems(player) - for name,count in pairs(PLAYER_SPAWN_START_ITEMS) do - player.insert({name=name, count=count}) + { { position.x - (CHUNK_SIZE * chunkDist), + position.y - (CHUNK_SIZE * chunkDist) }, + { position.x + (CHUNK_SIZE * chunkDist), + position.y + (CHUNK_SIZE * chunkDist) } }) +end + +---Gives the player the respawn items if there are any +---@param player LuaPlayer +---@return nil +function GivePlayerRespawnItems(player) + local surface_name = player.surface.name + if (global.ocfg.surfaces_config[surface_name] == nil) then + error("GivePlayerRespawnItems - Missing surface config! " .. surface_name) + return end - if global.ocfg.enable_power_armor_start then - GiveQuickStartPowerArmor(player) - elseif global.ocfg.enable_modular_armor_start then - GiveQuickStartModularArmor(player) - end -end + local respawnItems = global.ocfg.surfaces_config[surface_name].starting_items.player_respawn_items --- Modular armor quick start -function GiveQuickStartModularArmor(player) - player.insert{name="modular-armor", count = 1} - - if player and player.get_inventory(defines.inventory.character_armor) ~= nil and player.get_inventory(defines.inventory.character_armor)[1] ~= nil then - local p_armor = player.get_inventory(defines.inventory.character_armor)[1].grid - if p_armor ~= nil then - p_armor.put({name = "personal-roboport-equipment"}) - p_armor.put({name = "battery-mk2-equipment"}) - p_armor.put({name = "personal-roboport-equipment"}) - for i=1,15 do - p_armor.put({name = "solar-panel-equipment"}) - end - end - player.insert{name="construction-robot", count = 40} - end + util.insert_safe(player, respawnItems) end --- Cheater's quick start -function GiveQuickStartPowerArmor(player) - player.insert{name="power-armor", count = 1} - - if player and player.get_inventory(defines.inventory.character_armor) ~= nil and player.get_inventory(defines.inventory.character_armor)[1] ~= nil then - local p_armor = player.get_inventory(defines.inventory.character_armor)[1].grid - if p_armor ~= nil then - p_armor.put({name = "fusion-reactor-equipment"}) - p_armor.put({name = "exoskeleton-equipment"}) - p_armor.put({name = "battery-mk2-equipment"}) - p_armor.put({name = "battery-mk2-equipment"}) - p_armor.put({name = "personal-roboport-mk2-equipment"}) - p_armor.put({name = "personal-roboport-mk2-equipment"}) - p_armor.put({name = "personal-roboport-mk2-equipment"}) - p_armor.put({name = "battery-mk2-equipment"}) - for i=1,7 do - p_armor.put({name = "solar-panel-equipment"}) - end - end - player.insert{name="construction-robot", count = 100} - player.insert{name="belt-immunity-equipment", count = 1} +---Gives the player the starter items if there are any +---@param player LuaPlayer +---@return nil +function GivePlayerStarterItems(player) + local surface_name = player.surface.name + if (global.ocfg.surfaces_config[surface_name] == nil) then + error("GivePlayerStarterItems - Missing surface config! " .. surface_name) + return end -end -TEST_KIT = { - {name="infinity-chest", count = 50}, - {name="infinity-pipe", count = 50}, - {name="electric-energy-interface", count = 50}, - {name="express-loader", count = 50}, - {name="express-transport-belt", count = 50}, -} - -function GiveTestKit(player) - for _,item in pairs(TEST_KIT) do - player.insert(item) - end -end + local startItems = global.ocfg.surfaces_config[surface_name].starting_items.player_start_items + + util.insert_safe(player, startItems) +end + +---Half-heartedly attempts to remove starter items from the player. Probably more trouble than it's worth. +---@param player LuaPlayer +---@return nil +function RemovePlayerStarterItems(player) + local surface_name = player.surface.name + if (global.ocfg.surfaces_config[surface_name]) ~= nil then + local startItems = global.ocfg.surfaces_config[surface_name].starting_items.player_start_items + util.remove_safe(player, startItems) + end +end + +--- Delete all chunks on a surface +--- @param surface LuaSurface +--- @return nil +function DeleteAllChunks(surface) + for chunk in surface.get_chunks() do + surface.delete_chunk({chunk.x, chunk.y}) + end +end + + +-- -- Modular armor quick start +-- function GiveQuickStartModularArmor(player) +-- player.insert{name="modular-armor", count = 1} + +-- if player and player.get_inventory(defines.inventory.character_armor) ~= nil and player.get_inventory(defines.inventory.character_armor)[1] ~= nil then +-- local p_armor = player.get_inventory(defines.inventory.character_armor)[1].grid +-- if p_armor ~= nil then +-- p_armor.put({name = "personal-roboport-equipment"}) +-- p_armor.put({name = "battery-mk2-equipment"}) +-- p_armor.put({name = "personal-roboport-equipment"}) +-- for i=1,15 do +-- p_armor.put({name = "solar-panel-equipment"}) +-- end +-- end +-- player.insert{name="construction-robot", count = 40} +-- end +-- end + +-- -- Cheater's quick start +-- function GiveQuickStartPowerArmor(player) +-- player.insert{name="power-armor", count = 1} + +-- if player and player.get_inventory(defines.inventory.character_armor) ~= nil and player.get_inventory(defines.inventory.character_armor)[1] ~= nil then +-- local p_armor = player.get_inventory(defines.inventory.character_armor)[1].grid +-- if p_armor ~= nil then +-- p_armor.put({name = "fusion-reactor-equipment"}) +-- p_armor.put({name = "exoskeleton-equipment"}) +-- p_armor.put({name = "battery-mk2-equipment"}) +-- p_armor.put({name = "battery-mk2-equipment"}) +-- p_armor.put({name = "personal-roboport-mk2-equipment"}) +-- p_armor.put({name = "personal-roboport-mk2-equipment"}) +-- p_armor.put({name = "personal-roboport-mk2-equipment"}) +-- p_armor.put({name = "battery-mk2-equipment"}) +-- for i=1,7 do +-- p_armor.put({name = "solar-panel-equipment"}) +-- end +-- end +-- player.insert{name="construction-robot", count = 100} +-- player.insert{name="belt-immunity-equipment", count = 1} +-- end +-- end + +-- TEST_KIT = { +-- {name="infinity-chest", count = 50}, +-- {name="infinity-pipe", count = 50}, +-- {name="electric-energy-interface", count = 50}, +-- {name="express-loader", count = 50}, +-- {name="express-transport-belt", count = 50}, +-- } + +-- function GiveTestKit(player) +-- for _,item in pairs(TEST_KIT) do +-- player.insert(item) +-- end +-- end -- Safer teleport +---@param player LuaPlayer +---@param surface LuaSurface +---@param target_pos MapPosition function SafeTeleport(player, surface, target_pos) local safe_pos = surface.find_non_colliding_position("character", target_pos, 15, 1) if (not safe_pos) then @@ -365,18 +485,22 @@ function SafeTeleport(player, surface, target_pos) end end --- Create area given point and radius-distance -function GetAreaFromPointAndDistance(point, dist) - local area = {left_top= - {x=point.x-dist, - y=point.y-dist}, - right_bottom= - {x=point.x+dist, - y=point.y+dist}} - return area -end +-- Duplicate function ?? +-- -- Create area given point and radius-distance +-- function GetAreaFromPointAndDistance(point, dist) +-- local area = {left_top= +-- {x=point.x-dist, +-- y=point.y-dist}, +-- right_bottom= +-- {x=point.x+dist, +-- y=point.y+dist}} +-- return area +-- end --- Check if given position is in area bounding box +---Check if given position is in area bounding box +---@param point MapPosition +---@param area BoundingBox +---@return boolean function CheckIfInArea(point, area) if ((point.x >= area.left_top.x) and (point.x < area.right_bottom.x)) then if ((point.y >= area.left_top.y) and (point.y < area.right_bottom.y)) then @@ -386,39 +510,72 @@ function CheckIfInArea(point, area) return false end --- Set all forces to ceasefire -function SetCeaseFireBetweenAllForces() - for name,team in pairs(game.forces) do - if name ~= "neutral" and name ~= "enemy" and name ~= global.ocore.abandoned_force then - for x,y in pairs(game.forces) do - if x ~= "neutral" and x ~= "enemy" and name ~= global.ocore.abandoned_force then - team.set_cease_fire(x,true) - end - end +---Configures the friend and cease fire relationships between all player forces. +---@param cease_fire boolean +---@param friends boolean +---@return nil +function ConfigurePlayerForceRelationships(cease_fire, friends) + local player_forces = {} + + for name, force in pairs(game.forces) do + if name ~= "neutral" and name ~= ABANDONED_FORCE_NAME and not TableContains(ENEMY_FORCES_NAMES, name) then + table.insert(player_forces, force) end end -end --- Set all forces to friendly -function SetFriendlyBetweenAllForces() - for name,team in pairs(game.forces) do - if name ~= "neutral" and name ~= "enemy" and name ~= global.ocore.abandoned_force then - for x,y in pairs(game.forces) do - if x ~= "neutral" and x ~= "enemy" and name ~= global.ocore.abandoned_force then - team.set_friend(x,true) - end + for _, force1 in pairs(player_forces) do + for _, force2 in pairs(player_forces) do + if force1.name ~= force2.name then + force1.set_cease_fire(force2, cease_fire) + force1.set_friend(force2, friends) + + force2.set_cease_fire(force1, cease_fire) + force2.set_friend(force1, friends) end end end end --- For each other player force, share a chat msg. +-- ---Set all forces to ceasefire +-- ---@return nil +-- function SetCeaseFireBetweenAllPlayerForces() +-- for name, team in pairs(game.forces) do +-- if name ~= "neutral" and name ~= ABANDONED_FORCE_NAME and not TableContains(ENEMY_FORCES_NAMES, name) then +-- for x, _ in pairs(game.forces) do +-- if x ~= "neutral" and x ~= ABANDONED_FORCE_NAME and not TableContains(ENEMY_FORCES_NAMES, x) then +-- team.set_cease_fire(x, true) +-- end +-- end +-- end +-- end +-- end + +-- ---Set all forces to friendly +-- ---@return nil +-- function SetFriendlyBetweenAllPlayerForces() +-- for name, team in pairs(game.forces) do +-- if name ~= "neutral" and name ~= ABANDONED_FORCE_NAME and not TableContains(ENEMY_FORCES_NAMES, name) then +-- for x, _ in pairs(game.forces) do +-- if x ~= "neutral" and x ~= ABANDONED_FORCE_NAME and not TableContains(ENEMY_FORCES_NAMES, x) then +-- team.set_friend(x, true) +-- end +-- end +-- end +-- end +-- end + + +---For each other player force, share a chat msg. +---@param player LuaPlayer +---@param msg LocalisedString +---@return nil function ShareChatBetweenForces(player, msg) for _,force in pairs(game.forces) do if (force ~= nil) then - if ((force.name ~= enemy) and - (force.name ~= neutral) and - (force.name ~= player) and + if ((force.name ~= "enemy") and + (force.name ~= "enemy-easy") and + (force.name ~= "neutral") and + (force.name ~= "player") and (force ~= player.force)) then force.print(player.name..": "..msg) end @@ -426,51 +583,52 @@ function ShareChatBetweenForces(player, msg) end end --- Merges force2 INTO force1 but keeps all research between both forces. -function MergeForcesKeepResearch(force1, force2) - for techName,luaTech in pairs(force2.technologies) do - if (luaTech.researched) then - force1.technologies[techName].researched = true - force1.technologies[techName].level = luaTech.level - end - end - game.merge_forces(force2, force1) -end - --- Undecorator -function RemoveDecorationsArea(surface, area) - surface.destroy_decoratives{area=area} -end - --- Remove fish -function RemoveFish(surface, area) - for _, entity in pairs(surface.find_entities_filtered{area = area, type="fish"}) do - entity.destroy() - end -end - --- Render a path -function RenderPath(path, ttl, players) - local last_pos = path[1].position - local color = {r = 1, g = 0, b = 0, a = 0.5} - - for i,v in pairs(path) do - if (i ~= 1) then - - color={r = 1/(1+(i%3)), g = 1/(1+(i%5)), b = 1/(1+(i%7)), a = 0.5} - rendering.draw_line{color=color, - width=2, - from=v.position, - to=last_pos, - surface=game.surfaces[GAME_SURFACE_NAME], - players=players, - time_to_live=ttl} - end - last_pos = v.position - end -end - --- Get a random 1 or -1 +-- -- Merges force2 INTO force1 but keeps all research between both forces. +-- function MergeForcesKeepResearch(force1, force2) +-- for techName,luaTech in pairs(force2.technologies) do +-- if (luaTech.researched) then +-- force1.technologies[techName].researched = true +-- force1.technologies[techName].level = luaTech.level +-- end +-- end +-- game.merge_forces(force2, force1) +-- end + +-- -- Undecorator +-- function RemoveDecorationsArea(surface, area) +-- surface.destroy_decoratives{area=area} +-- end + +-- -- Remove fish +-- function RemoveFish(surface, area) +-- for _, entity in pairs(surface.find_entities_filtered{area = area, type="fish"}) do +-- entity.destroy() +-- end +-- end + +-- -- Render a path +-- function RenderPath(path, ttl, players) +-- local last_pos = path[1].position +-- local color = {r = 1, g = 0, b = 0, a = 0.5} + +-- for i,v in pairs(path) do +-- if (i ~= 1) then + +-- color={r = 1/(1+(i%3)), g = 1/(1+(i%5)), b = 1/(1+(i%7)), a = 0.5} +-- rendering.draw_line{color=color, +-- width=2, +-- from=v.position, +-- to=last_pos, +-- surface=game.surfaces[GAME_SURFACE_NAME], +-- players=players, +-- time_to_live=ttl} +-- end +-- last_pos = v.position +-- end +-- end + +---Get a random 1 or -1 +---@return number function RandomNegPos() if (math.random(0,1) == 1) then return 1 @@ -479,19 +637,27 @@ function RandomNegPos() end end --- Create a random direction vector to look in +---Create a random direction vector to look in, returns normalized vector +---@return MapPosition function GetRandomVector() local randVec = {x=0,y=0} while ((randVec.x == 0) and (randVec.y == 0)) do - randVec.x = math.random(-3,3) - randVec.y = math.random(-3,3) + randVec.x = math.random() * 2 - 1 + randVec.y = math.random() * 2 - 1 end + -- Normalize the vector + local magnitude = math.sqrt((randVec.x^2) + (randVec.y^2)) + randVec.x = randVec.x / magnitude + randVec.y = randVec.y / magnitude log("direction: x=" .. randVec.x .. ", y=" .. randVec.y) return randVec end --- Check for ungenerated chunks around a specific chunk --- +/- chunkDist in x and y directions +---Check for ungenerated chunks around a specific chunk +/- chunkDist in x and y directions +---@param chunkPos MapPosition +---@param chunkDist integer +---@param surface LuaSurface +---@return boolean function IsChunkAreaUngenerated(chunkPos, chunkDist, surface) for x=-chunkDist, chunkDist do for y=-chunkDist, chunkDist do @@ -506,747 +672,830 @@ function IsChunkAreaUngenerated(chunkPos, chunkDist, surface) end -- Clear out enemies around an area with a certain distance +---@param pos MapPosition +---@param safeDist number +---@param surface LuaSurface function ClearNearbyEnemies(pos, safeDist, surface) - local safeArea = {left_top= - {x=pos.x-safeDist, - y=pos.y-safeDist}, - right_bottom= - {x=pos.x+safeDist, - y=pos.y+safeDist}} - - for _, entity in pairs(surface.find_entities_filtered{area = safeArea, force = "enemy"}) do + local safeArea = { + left_top = + { + x = pos.x - safeDist, + y = pos.y - safeDist + }, + right_bottom = + { + x = pos.x + safeDist, + y = pos.y + safeDist + } + } + + for _, entity in pairs(surface.find_entities_filtered { area = safeArea, force = "enemy" }) do entity.destroy() end end --- Function to find coordinates of ungenerated map area in a given direction --- starting from the center of the map -function FindMapEdge(directionVec, surface) - local position = {x=0,y=0} - local chunkPos = {x=0,y=0} +-- ---Function to find coordinates of ungenerated map area in a given direction starting from the center of the map +-- ---@param direction_vector MapPosition +-- ---@param surface LuaSurface +-- ---@return MapPosition +-- function FindMapEdge(direction_vector, surface) +-- local position = {x=0,y=0} +-- local chunk_position = {x=0,y=0} - -- Keep checking chunks in the direction of the vector - while(true) do +-- -- Keep checking chunks in the direction of the vector +-- while(true) do - -- Set some absolute limits. - if ((math.abs(chunkPos.x) > 1000) or (math.abs(chunkPos.y) > 1000)) then - break +-- -- Set some absolute limits. +-- if ((math.abs(chunk_position.x) > 1000) or (math.abs(chunk_position.y) > 1000)) then +-- break - -- If chunk is already generated, keep looking - elseif (surface.is_chunk_generated(chunkPos)) then - chunkPos.x = chunkPos.x + directionVec.x - chunkPos.y = chunkPos.y + directionVec.y +-- -- If chunk is already generated, keep looking +-- elseif (surface.is_chunk_generated(chunk_position)) then +-- chunk_position.x = chunk_position.x + direction_vector.x +-- chunk_position.y = chunk_position.y + direction_vector.y - -- Found a possible ungenerated area - else - - chunkPos.x = chunkPos.x + directionVec.x - chunkPos.y = chunkPos.y + directionVec.y - - -- Check there are no generated chunks in a 10x10 area. - if IsChunkAreaUngenerated(chunkPos, 10, surface) then - position.x = (chunkPos.x*CHUNK_SIZE) + (CHUNK_SIZE/2) - position.y = (chunkPos.y*CHUNK_SIZE) + (CHUNK_SIZE/2) - break - end - end - end - - -- log("spawn: x=" .. position.x .. ", y=" .. position.y) - return position -end +-- -- Found a possible ungenerated area +-- else --- Find random coordinates within a given distance away --- maxTries is the recursion limit basically. -function FindUngeneratedCoordinates(minDistChunks, maxDistChunks, surface) - local position = {x=0,y=0} - local chunkPos = {x=0,y=0} +-- chunk_position.x = chunk_position.x + direction_vector.x +-- chunk_position.y = chunk_position.y + direction_vector.y - local maxTries = 100 - local tryCounter = 0 +-- -- Check there are no generated chunks in a 10x10 area. +-- if IsChunkAreaUngenerated(chunk_position, 10, surface) then +-- position.x = (chunk_position.x*CHUNK_SIZE) + (CHUNK_SIZE/2) +-- position.y = (chunk_position.y*CHUNK_SIZE) + (CHUNK_SIZE/2) +-- break +-- end +-- end +-- end - local minDistSqr = minDistChunks^2 - local maxDistSqr = maxDistChunks^2 +-- -- log("spawn: x=" .. position.x .. ", y=" .. position.y) +-- return position +-- end - while(true) do - chunkPos.x = math.random(0,maxDistChunks) * RandomNegPos() - chunkPos.y = math.random(0,maxDistChunks) * RandomNegPos() - - local distSqrd = chunkPos.x^2 + chunkPos.y^2 - - -- Enforce a max number of tries - tryCounter = tryCounter + 1 - if (tryCounter > maxTries) then - log("FindUngeneratedCoordinates - Max Tries Hit!") - break - - -- Check that the distance is within the min,max specified - elseif ((distSqrd < minDistSqr) or (distSqrd > maxDistSqr)) then - -- Keep searching! - -- Check there are no generated chunks in a 10x10 area. - elseif IsChunkAreaUngenerated(chunkPos, CHECK_SPAWN_UNGENERATED_CHUNKS_RADIUS, surface) then - position.x = (chunkPos.x*CHUNK_SIZE) + (CHUNK_SIZE/2) - position.y = (chunkPos.y*CHUNK_SIZE) + (CHUNK_SIZE/2) - break -- SUCCESS - end - end +---Pick a random direction, go at least the minimum distance, and start looking for ungenerated chunks +---We try a few times (hardcoded) and then try a different random direction if we fail (up to max_tries) +---@param surface LuaSurface +---@param minimum_distance_chunks number Distance in chunks to start looking for ungenerated chunks +---@param max_tries integer Maximum number of tries to find a spawn point +---@return MapPosition +function FindUngeneratedCoordinates(surface, minimum_distance_chunks, max_tries) - log("spawn: x=" .. position.x .. ", y=" .. position.y) - return position -end - --- General purpose function for removing a particular recipe -function RemoveRecipe(force, recipeName) - local recipes = force.recipes - if recipes[recipeName] then - recipes[recipeName].enabled = false - end -end - --- General purpose function for adding a particular recipe -function AddRecipe(force, recipeName) - local recipes = force.recipes - if recipes[recipeName] then - recipes[recipeName].enabled = true - end -end - --- General command for disabling a tech. -function DisableTech(force, techName) - if force.technologies[techName] then - force.technologies[techName].enabled = false - force.technologies[techName].visible_when_disabled = true - end -end - --- General command for enabling a tech. -function EnableTech(force, techName) - if force.technologies[techName] then - force.technologies[techName].enabled = true - end -end - - --- Get an area given a position and distance. --- Square length = 2x distance -function GetAreaAroundPos(pos, dist) + --- Get a random vector, figure out how many times to multiply it to get the minimum distance + local direction_vector = GetRandomVector() + local start_distance_tiles = minimum_distance_chunks * CHUNK_SIZE + + local final_position = {x=0,y=0} + local tries_remaining = max_tries - 1 - return {left_top= - {x=pos.x-dist, - y=pos.y-dist}, - right_bottom= - {x=pos.x+dist, - y=pos.y+dist}} -end + -- Starting search position + local search_pos = { + x=direction_vector.x * start_distance_tiles, + y=direction_vector.y * start_distance_tiles + } --- Gets chunk position of a tile. -function GetChunkPosFromTilePos(tile_pos) - return {x=math.floor(tile_pos.x/32), y=math.floor(tile_pos.y/32)} -end + -- We check up to THIS many times, each jump moves out by minimum_distance_to_existing_chunks + local jumps_count = 3 -function GetCenterTilePosFromChunkPos(c_pos) - return {x=c_pos.x*32 + 16, y=c_pos.y*32 + 16} -end + local minimum_distance_to_existing_chunks = global.ocfg.gameplay.minimum_distance_to_existing_chunks --- Get the left_top -function GetChunkTopLeft(pos) - return {x=pos.x-(pos.x % 32), y=pos.y-(pos.y % 32)} -end + -- Keep checking chunks in the direction of the vector, assumes this terminates... + while(true) do --- Get area given chunk -function GetAreaFromChunkPos(chunk_pos) - return {left_top={x=chunk_pos.x*32, y=chunk_pos.y*32}, - right_bottom={x=chunk_pos.x*32+31, y=chunk_pos.y*32+31}} -end + local chunk_position = GetChunkPosFromTilePos(search_pos) --- Removes the entity type from the area given -function RemoveInArea(surface, area, type) - for key, entity in pairs(surface.find_entities_filtered{area=area, type= type}) do - if entity.valid and entity and entity.position then - entity.destroy() - end - end -end + if (jumps_count <= 0) then --- Removes the entity type from the area given --- Only if it is within given distance from given position. -function RemoveInCircle(surface, area, type, pos, dist) - for key, entity in pairs(surface.find_entities_filtered{area=area, type= type}) do - if entity.valid and entity and entity.position then - if ((pos.x - entity.position.x)^2 + (pos.y - entity.position.y)^2 < dist^2) then - entity.destroy() + if (tries_remaining > 0) then + return FindUngeneratedCoordinates(surface, minimum_distance_chunks, tries_remaining) + else + log("WARNING - FindUngeneratedCoordinates - Hit max distance!") + break end - end - end -end - --- For easy local testing of map gen settings. Just set what you want and uncomment usage in CreateGameSurface! -function SurfaceSettingsHelper(settings) - - settings.terrain_segmentation = 4 - settings.water = 3 - settings.starting_area = 0 - - local r_freq = 1.20 - local r_rich = 5.00 - local r_size = 0.18 - - settings.autoplace_controls["coal"].frequency = r_freq - settings.autoplace_controls["coal"].richness = r_rich - settings.autoplace_controls["coal"].size = r_size - settings.autoplace_controls["copper-ore"].frequency = r_freq - settings.autoplace_controls["copper-ore"].richness = r_rich - settings.autoplace_controls["copper-ore"].size = r_size - settings.autoplace_controls["crude-oil"].frequency = r_freq - settings.autoplace_controls["crude-oil"].richness = r_rich - settings.autoplace_controls["crude-oil"].size = r_size - settings.autoplace_controls["iron-ore"].frequency = r_freq - settings.autoplace_controls["iron-ore"].richness = r_rich - settings.autoplace_controls["iron-ore"].size = r_size - settings.autoplace_controls["stone"].frequency = r_freq - settings.autoplace_controls["stone"].richness = r_rich - settings.autoplace_controls["stone"].size = r_size - settings.autoplace_controls["uranium-ore"].frequency = r_freq*0.5 - settings.autoplace_controls["uranium-ore"].richness = r_rich - settings.autoplace_controls["uranium-ore"].size = r_size - - settings.autoplace_controls["enemy-base"].frequency = 0.80 - settings.autoplace_controls["enemy-base"].richness = 0.70 - settings.autoplace_controls["enemy-base"].size = 0.70 - - settings.autoplace_controls["trees"].frequency = 1.00 - settings.autoplace_controls["trees"].richness = 1.00 - settings.autoplace_controls["trees"].size = 1.00 - - settings.cliff_settings.cliff_elevation_0 = 3 - settings.cliff_settings.cliff_elevation_interval = 200 - settings.cliff_settings.richness = 3 - - settings.property_expression_names["control-setting:aux:bias"] = "0.00" - settings.property_expression_names["control-setting:aux:frequency:multiplier"] = "5.00" - settings.property_expression_names["control-setting:moisture:bias"] = "0.40" - settings.property_expression_names["control-setting:moisture:frequency:multiplier"] = "50" - - return settings -end --- Create another surface so that we can modify map settings and not have a screwy nauvis map. -function CreateGameSurface() + -- If chunk is already generated, keep looking further out + elseif (surface.is_chunk_generated(chunk_position)) then - if (GAME_SURFACE_NAME ~= "nauvis") then + -- For debugging, ping the map + -- SendBroadcastMsg("GENERATED: " .. GetGPStext(surface.name, {x=chunk_position.x*32, y=chunk_position.y*32})) - -- Get starting surface settings. - local nauvis_settings = game.surfaces["nauvis"].map_gen_settings + -- Move out a bit more to give some space and then check the surrounding area + search_pos.x = search_pos.x + (direction_vector.x * CHUNK_SIZE * minimum_distance_to_existing_chunks) + search_pos.y = search_pos.y + (direction_vector.y * CHUNK_SIZE * minimum_distance_to_existing_chunks) - if global.ocfg.enable_vanilla_spawns then - nauvis_settings.starting_points = CreateVanillaSpawns(global.ocfg.vanilla_spawn_count, global.ocfg.vanilla_spawn_spacing) - - -- ENFORCE ISLAND MAP GEN - if (global.ocfg.silo_islands) then - nauvis_settings.property_expression_names.elevation = "0_17-island" - end - end + -- Found a possible ungenerated area + elseif IsChunkAreaUngenerated(chunk_position, minimum_distance_to_existing_chunks, surface) then - -- Enable this to test things out easily. - -- nauvis_settings = SurfaceSettingsHelper(nauvis_settings) + -- For debugging, ping the map + -- SendBroadcastMsg("SUCCESS: " .. GetGPStext(surface.name, {x=chunk_position.x*32, y=chunk_position.y*32})) - -- Create new game surface - local s = game.create_surface(GAME_SURFACE_NAME, nauvis_settings) + -- Place the spawn in the center of a chunk + final_position.x = (chunk_position.x * CHUNK_SIZE) + (CHUNK_SIZE/2) + final_position.y = (chunk_position.y * CHUNK_SIZE) + (CHUNK_SIZE/2) + break + + -- The area around the chunk is not clear, keep looking + else - end + -- For debugging, ping the map + -- SendBroadcastMsg("NOT CLEAR: " .. GetGPStext(surface.name, {x=chunk_position.x*32, y=chunk_position.y*32})) - -- Add surface and safe areas - if global.ocfg.enable_regrowth then - RegrowthMarkAreaSafeGivenChunkPos({x=0,y=0}, 4, true) - end -end + -- Move out a bit more to give some space and then check the surrounding area + search_pos.x = search_pos.x + (direction_vector.x * CHUNK_SIZE * minimum_distance_to_existing_chunks) + search_pos.y = search_pos.y + (direction_vector.y * CHUNK_SIZE * minimum_distance_to_existing_chunks) + end -function CreateTileArrow(surface, pos, type) - - tiles = {} - - if (type == "LEFT") then - table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x, pos.y}}) - table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+1, pos.y}}) - table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+2, pos.y}}) - table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+3, pos.y}}) - table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x, pos.y+1}}) - table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+1, pos.y+1}}) - table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+2, pos.y+1}}) - table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+3, pos.y+1}}) - elseif (type == "RIGHT") then - table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x, pos.y}}) - table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+1, pos.y}}) - table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+2, pos.y}}) - table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+3, pos.y}}) - table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x, pos.y+1}}) - table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+1, pos.y+1}}) - table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+2, pos.y+1}}) - table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+3, pos.y+1}}) + jumps_count = jumps_count - 1 end - - surface.set_tiles(tiles, true) -end - --- Allowed colors: red, green, blue, orange, yellow, pink, purple, black, brown, cyan, acid -function CreateFixedColorTileArea(surface, area, color) - tiles = {} - - for i=area.left_top.x,area.right_bottom.x do - for j=area.left_top.y,area.right_bottom.y do - table.insert(tiles, {name = color.."-refined-concrete", position = {i,j}}) - end + if (final_position.x == 0 and final_position.y == 0) then + log("WARNING! FindUngeneratedCoordinates - Failed to find a spawn point!") end - surface.set_tiles(tiles, true) + return final_position end --- Find closest player-owned entity -function FindClosestPlayerOwnedEntity(player, name, radius) +-- -- General purpose function for removing a particular recipe +-- function RemoveRecipe(force, recipeName) +-- local recipes = force.recipes +-- if recipes[recipeName] then +-- recipes[recipeName].enabled = false +-- end +-- end - local entities = player.surface.find_entities_filtered{position=player.position, - radius=radius, - name=name, - force=player.force} - if (not entities or (#entities == 0)) then return nil end +-- -- General purpose function for adding a particular recipe +-- function AddRecipe(force, recipeName) +-- local recipes = force.recipes +-- if recipes[recipeName] then +-- recipes[recipeName].enabled = true +-- end +-- end - return player.surface.get_closest(player.position, entities) -end +-- -- General command for disabling a tech. +-- function DisableTech(force, techName) +-- if force.technologies[techName] then +-- force.technologies[techName].enabled = false +-- force.technologies[techName].visible_when_disabled = true +-- end +-- end --------------------------------------------------------------------------------- --- Functions for removing/modifying enemies --------------------------------------------------------------------------------- +-- -- General command for enabling a tech. +-- function EnableTech(force, techName) +-- if force.technologies[techName] then +-- force.technologies[techName].enabled = true +-- end +-- end --- Convenient way to remove aliens, just provide an area -function RemoveAliensInArea(surface, area) - for _, entity in pairs(surface.find_entities_filtered{area = area, force = "enemy"}) do - entity.destroy() - end -end --- Make an area safer --- Reduction factor divides the enemy spawns by that number. 2 = half, 3 = third, etc... --- Also removes all big and huge worms in that area -function ReduceAliensInArea(surface, area, reductionFactor) - for _, entity in pairs(surface.find_entities_filtered{area = area, force = "enemy"}) do - if (math.random(0,reductionFactor) > 0) then - entity.destroy() - end - end +---Get a square area given a position and distance. Square length = 2x distance +---@param pos MapPosition +---@param dist number +---@return BoundingBox +function GetAreaAroundPos(pos, dist) + return { + left_top = + { + x = pos.x - dist, + y = pos.y - dist + }, + right_bottom = + { + x = pos.x + dist, + y = pos.y + dist + } + } +end + +---Gets chunk position of a tile. +---@param tile_pos TilePosition +---@return ChunkPosition +function GetChunkPosFromTilePos(tile_pos) + return {x=math.floor(tile_pos.x/32), y=math.floor(tile_pos.y/32)} end --- Downgrades worms in an area based on chance. --- 100% small would mean all worms are changed to small. -function DowngradeWormsInArea(surface, area, small_percent, medium_percent, big_percent) - - local worm_types = {"small-worm-turret", "medium-worm-turret", "big-worm-turret", "behemoth-worm-turret"} - - for _, entity in pairs(surface.find_entities_filtered{area = area, name = worm_types}) do +-- function GetCenterTilePosFromChunkPos(c_pos) +-- return {x=c_pos.x*32 + 16, y=c_pos.y*32 + 16} +-- end - -- Roll a number between 0-100 - local rand_percent = math.random(0,100) - local worm_pos = entity.position - local worm_name = entity.name +-- -- Get the left_top +-- function GetChunkTopLeft(pos) +-- return {x=pos.x-(pos.x % 32), y=pos.y-(pos.y % 32)} +-- end - -- If number is less than small percent, change to small - if (rand_percent <= small_percent) then - if (not (worm_name == "small-worm-turret")) then - entity.destroy() - surface.create_entity{name = "small-worm-turret", position = worm_pos, force = game.forces.enemy} - end - - -- ELSE If number is less than medium percent, change to small - elseif (rand_percent <= medium_percent) then - if (not (worm_name == "medium-worm-turret")) then - entity.destroy() - surface.create_entity{name = "medium-worm-turret", position = worm_pos, force = game.forces.enemy} - end +-- -- Get area given chunk +-- function GetAreaFromChunkPos(chunk_pos) +-- return {left_top={x=chunk_pos.x*32, y=chunk_pos.y*32}, +-- right_bottom={x=chunk_pos.x*32+31, y=chunk_pos.y*32+31}} +-- end - -- ELSE If number is less than big percent, change to small - elseif (rand_percent <= big_percent) then - if (not (worm_name == "big-worm-turret")) then +-- Removes the entity type from the area given +-- function RemoveInArea(surface, area, type) +-- for key, entity in pairs(surface.find_entities_filtered{area=area, type= type}) do +-- if entity.valid and entity and entity.position then +-- entity.destroy() +-- end +-- end +-- end + +---Removes the entity type from the area given. Only if it is within given distance from given position. +---@param surface LuaSurface +---@param area BoundingBox +---@param type string|string[] +---@param pos MapPosition +---@param dist number +---@return nil +function RemoveInCircle(surface, area, type, pos, dist) + for _, entity in pairs(surface.find_entities_filtered { area = area, type = type }) do + if entity.valid and entity and entity.position then + if ((pos.x - entity.position.x) ^ 2 + (pos.y - entity.position.y) ^ 2 < dist ^ 2) then entity.destroy() - surface.create_entity{name = "big-worm-turret", position = worm_pos, force = game.forces.enemy} end - - -- ELSE ignore it. end end end -function DowngradeWormsDistanceBasedOnChunkGenerate(event) - if (getDistance({x=0,y=0}, event.area.left_top) < (global.ocfg.near_dist_end*CHUNK_SIZE)) then - DowngradeWormsInArea(event.surface, event.area, 100, 100, 100) - elseif (getDistance({x=0,y=0}, event.area.left_top) < (global.ocfg.far_dist_start*CHUNK_SIZE)) then - DowngradeWormsInArea(event.surface, event.area, 50, 90, 100) - elseif (getDistance({x=0,y=0}, event.area.left_top) < (global.ocfg.far_dist_end*CHUNK_SIZE)) then - DowngradeWormsInArea(event.surface, event.area, 20, 80, 97) - else - DowngradeWormsInArea(event.surface, event.area, 0, 20, 90) - end -end - --- A function to help me remove worms in an area. --- Yeah kind of an unecessary wrapper, but makes my life easier to remember the worm types. -function RemoveWormsInArea(surface, area, small, medium, big, behemoth) - local worm_types = {} - - if (small) then - table.insert(worm_types, "small-worm-turret") - end - if (medium) then - table.insert(worm_types, "medium-worm-turret") - end - if (big) then - table.insert(worm_types, "big-worm-turret") - end - if (behemoth) then - table.insert(worm_types, "behemoth-worm-turret") - end - - -- Destroy - if (TableLength(worm_types) > 0) then - for _, entity in pairs(surface.find_entities_filtered{area = area, name = worm_types}) do +---Removes the entity type from the area given. Only if it is within given distance from given position. +---@param surface LuaSurface +---@param area BoundingBox +---@param type string|string[] +---@param pos MapPosition +---@param dist number +---@return nil +function RemoveInSquare(surface, area, type, pos, dist) + for _, entity in pairs(surface.find_entities_filtered { area = area, type = type }) do + if entity.valid and entity and entity.position then + local max_distance = math.max(math.abs(pos.x - entity.position.x), math.abs(pos.y - entity.position.y)) + if (max_distance < dist) then entity.destroy() - end - else - log("RemoveWormsInArea had empty worm_types list!") - end -end - --- Add Long Reach to Character -function GivePlayerLongReach(player) - player.character.character_build_distance_bonus = BUILD_DIST_BONUS - player.character.character_reach_distance_bonus = REACH_DIST_BONUS - -- player.character.character_resource_reach_distance_bonus = RESOURCE_DIST_BONUS -end - --- General purpose cover an area in tiles. -function CoverAreaInTiles(surface, area, tile_name) - tiles = {} - for x = area.left_top.x,area.left_top.x+31 do - for y = area.left_top.y,area.left_top.y+31 do - table.insert(tiles, {name = tile_name, position = {x=x, y=y}}) - end - end - surface.set_tiles(tiles, true) -end - --------------------------------------------------------------------------------- --- Anti-griefing Stuff & Gravestone (My own version) --------------------------------------------------------------------------------- -function AntiGriefing(force) - force.zoom_to_world_deconstruction_planner_enabled=false - SetForceGhostTimeToLive(force) - -- TODO: Mess with permission groups and shit -end - -function SetForceGhostTimeToLive(force) - if global.ocfg.ghost_ttl ~= 0 then - force.ghost_time_to_live = global.ocfg.ghost_ttl+1 - end -end - -function SetItemBlueprintTimeToLive(event) - local type = event.created_entity.type - if type == "entity-ghost" or type == "tile-ghost" then - if global.ocfg.ghost_ttl ~= 0 then - event.created_entity.time_to_live = global.ocfg.ghost_ttl + end end end end --------------------------------------------------------------------------------- --- Gravestone soft mod. With my own modifications/improvements. --------------------------------------------------------------------------------- --- Return steel chest entity (or nil) -function DropEmptySteelChest(player) - local pos = player.surface.find_non_colliding_position("steel-chest", player.position, 15, 1) - if not pos then - return nil - end - local grave = player.surface.create_entity{name="steel-chest", position=pos, force="neutral"} - return grave -end +-- -- For easy local testing of map gen settings. Just set what you want and uncomment usage in CreateGameSurface! +-- function SurfaceSettingsHelper(settings) + +-- settings.terrain_segmentation = 4 +-- settings.water = 3 +-- settings.starting_area = 0 + +-- local r_freq = 1.20 +-- local r_rich = 5.00 +-- local r_size = 0.18 + +-- settings.autoplace_controls["coal"].frequency = r_freq +-- settings.autoplace_controls["coal"].richness = r_rich +-- settings.autoplace_controls["coal"].size = r_size +-- settings.autoplace_controls["copper-ore"].frequency = r_freq +-- settings.autoplace_controls["copper-ore"].richness = r_rich +-- settings.autoplace_controls["copper-ore"].size = r_size +-- settings.autoplace_controls["crude-oil"].frequency = r_freq +-- settings.autoplace_controls["crude-oil"].richness = r_rich +-- settings.autoplace_controls["crude-oil"].size = r_size +-- settings.autoplace_controls["iron-ore"].frequency = r_freq +-- settings.autoplace_controls["iron-ore"].richness = r_rich +-- settings.autoplace_controls["iron-ore"].size = r_size +-- settings.autoplace_controls["stone"].frequency = r_freq +-- settings.autoplace_controls["stone"].richness = r_rich +-- settings.autoplace_controls["stone"].size = r_size +-- settings.autoplace_controls["uranium-ore"].frequency = r_freq*0.5 +-- settings.autoplace_controls["uranium-ore"].richness = r_rich +-- settings.autoplace_controls["uranium-ore"].size = r_size + +-- settings.autoplace_controls["enemy-base"].frequency = 0.80 +-- settings.autoplace_controls["enemy-base"].richness = 0.70 +-- settings.autoplace_controls["enemy-base"].size = 0.70 + +-- settings.autoplace_controls["trees"].frequency = 1.00 +-- settings.autoplace_controls["trees"].richness = 1.00 +-- settings.autoplace_controls["trees"].size = 1.00 + +-- settings.cliff_settings.cliff_elevation_0 = 3 +-- settings.cliff_settings.cliff_elevation_interval = 200 +-- settings.cliff_settings.richness = 3 + +-- settings.property_expression_names["control-setting:aux:bias"] = "0.00" +-- settings.property_expression_names["control-setting:aux:frequency:multiplier"] = "5.00" +-- settings.property_expression_names["control-setting:moisture:bias"] = "0.40" +-- settings.property_expression_names["control-setting:moisture:frequency:multiplier"] = "50" + +-- return settings +-- end + +-- -- Create another surface so that we can modify map settings and not have a screwy nauvis map. +-- function CreateGameSurface() + +-- if (GAME_SURFACE_NAME ~= "nauvis") then + +-- -- Get starting surface settings. +-- local nauvis_settings = game.surfaces["nauvis"].map_gen_settings + +-- if global.ocfg.enable_vanilla_spawns then +-- nauvis_settings.starting_points = CreateVanillaSpawns(global.ocfg.vanilla_spawn_count, global.ocfg.vanilla_spawn_spacing) + +-- -- ENFORCE ISLAND MAP GEN +-- if (global.ocfg.silo_islands) then +-- nauvis_settings.property_expression_names.elevation = "0_17-island" +-- end +-- end + +-- -- Enable this to test things out easily. +-- -- nauvis_settings = SurfaceSettingsHelper(nauvis_settings) + +-- -- Create new game surface +-- local s = game.create_surface(GAME_SURFACE_NAME, nauvis_settings) + +-- end + +-- -- Add surface and safe areas +-- if global.ocfg.enable_regrowth then +-- RegrowthMarkAreaSafeGivenChunkPos({x=0,y=0}, 4, true) +-- end +-- end + +-- function CreateTileArrow(surface, pos, type) + +-- tiles = {} + +-- if (type == "LEFT") then +-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x, pos.y}}) +-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+1, pos.y}}) +-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+2, pos.y}}) +-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+3, pos.y}}) +-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x, pos.y+1}}) +-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+1, pos.y+1}}) +-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+2, pos.y+1}}) +-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+3, pos.y+1}}) +-- elseif (type == "RIGHT") then +-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x, pos.y}}) +-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+1, pos.y}}) +-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+2, pos.y}}) +-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+3, pos.y}}) +-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x, pos.y+1}}) +-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+1, pos.y+1}}) +-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+2, pos.y+1}}) +-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+3, pos.y+1}}) +-- end + +-- surface.set_tiles(tiles, true) +-- end + +-- -- Allowed colors: red, green, blue, orange, yellow, pink, purple, black, brown, cyan, acid +-- function CreateFixedColorTileArea(surface, area, color) + +-- tiles = {} + +-- for i=area.left_top.x,area.right_bottom.x do +-- for j=area.left_top.y,area.right_bottom.y do +-- table.insert(tiles, {name = color.."-refined-concrete", position = {i,j}}) +-- end +-- end + +-- surface.set_tiles(tiles, true) +-- end + +-- -- Find closest player-owned entity +-- function FindClosestPlayerOwnedEntity(player, name, radius) + +-- local entities = player.surface.find_entities_filtered{position=player.position, +-- radius=radius, +-- name=name, +-- force=player.force} +-- if (not entities or (#entities == 0)) then return nil end + +-- return player.surface.get_closest(player.position, entities) +-- end + +-- -- Add Long Reach to Character +-- function GivePlayerLongReach(player) +-- player.character.character_build_distance_bonus = BUILD_DIST_BONUS +-- player.character.character_reach_distance_bonus = REACH_DIST_BONUS +-- -- player.character.character_resource_reach_distance_bonus = RESOURCE_DIST_BONUS +-- end + +-- -- General purpose cover an area in tiles. +-- function CoverAreaInTiles(surface, area, tile_name) +-- tiles = {} +-- for x = area.left_top.x,area.left_top.x+31 do +-- for y = area.left_top.y,area.left_top.y+31 do +-- table.insert(tiles, {name = tile_name, position = {x=x, y=y}}) +-- end +-- end +-- surface.set_tiles(tiles, true) +-- end + +-- -------------------------------------------------------------------------------- +-- -- Anti-griefing Stuff & Gravestone (My own version) +-- -------------------------------------------------------------------------------- +-- function AntiGriefing(force) +-- force.zoom_to_world_deconstruction_planner_enabled=false +-- SetForceGhostTimeToLive(force) +-- end + +-- function SetForceGhostTimeToLive(force) +-- if global.ocfg.ghost_ttl ~= 0 then +-- force.ghost_time_to_live = global.ocfg.ghost_ttl+1 +-- end +-- end + +-- function SetItemBlueprintTimeToLive(event) +-- local type = event.created_entity.type +-- if type == "entity-ghost" or type == "tile-ghost" then +-- if global.ocfg.ghost_ttl ~= 0 then +-- event.created_entity.time_to_live = global.ocfg.ghost_ttl +-- end +-- end +-- end + +-- -------------------------------------------------------------------------------- +-- -- Gravestone soft mod. With my own modifications/improvements. +-- -------------------------------------------------------------------------------- +-- -- Return steel chest entity (or nil) +-- function DropEmptySteelChest(player) +-- local pos = player.surface.find_non_colliding_position("steel-chest", player.position, 15, 1) +-- if not pos then +-- return nil +-- end +-- local grave = player.surface.create_entity{name="steel-chest", position=pos, force="neutral"} +-- return grave +-- end + +-- function DropGravestoneChests(player) + +-- local grave +-- local count = 0 + +-- -- Make sure we save stuff we're holding in our hands. +-- player.clean_cursor() + +-- -- Loop through a players different inventories +-- -- Put it all into a chest. +-- -- If the chest is full, create a new chest. +-- for i, id in ipairs{ +-- defines.inventory.character_armor, +-- defines.inventory.character_main, +-- defines.inventory.character_guns, +-- defines.inventory.character_ammo, +-- defines.inventory.character_vehicle, +-- defines.inventory.character_trash} do + +-- local inv = player.get_inventory(id) + +-- -- No idea how inv can be nil sometimes...? +-- if (inv ~= nil) then +-- if ((#inv > 0) and not inv.is_empty()) then +-- for j = 1, #inv do +-- if inv[j].valid_for_read then + +-- -- Create a chest when counter is reset +-- if (count == 0) then +-- grave = DropEmptySteelChest(player) +-- if (grave == nil) then +-- -- player.print("Not able to place a chest nearby! Some items lost!") +-- return +-- end +-- grave_inv = grave.get_inventory(defines.inventory.chest) +-- end +-- count = count + 1 + +-- -- Copy the item stack into a chest slot. +-- grave_inv[count].set_stack(inv[j]) + +-- -- Reset counter when chest is full +-- if (count == #grave_inv) then +-- count = 0 +-- end +-- end +-- end +-- end + +-- -- Clear the player inventory so we don't have duplicate items lying around. +-- inv.clear() +-- end +-- end + +-- if (grave ~= nil) then +-- player.print("Successfully dropped your items into a chest! Go get them quick!") +-- end +-- end + +-- -- Dump player items into a chest after the body expires. +-- function DropGravestoneChestFromCorpse(corpse) +-- if ((corpse == nil) or (corpse.character_corpse_player_index == nil)) then return end + +-- local grave, grave_inv +-- local count = 0 + +-- local inv = corpse.get_inventory(defines.inventory.character_corpse) + +-- -- No idea how inv can be nil sometimes...? +-- if (inv ~= nil) then +-- if ((#inv > 0) and not inv.is_empty()) then +-- for j = 1, #inv do +-- if inv[j].valid_for_read then + +-- -- Create a chest when counter is reset +-- if (count == 0) then +-- grave = DropEmptySteelChest(corpse) +-- if (grave == nil) then +-- -- player.print("Not able to place a chest nearby! Some items lost!") +-- return +-- end +-- grave_inv = grave.get_inventory(defines.inventory.chest) +-- end +-- count = count + 1 + +-- -- Copy the item stack into a chest slot. +-- grave_inv[count].set_stack(inv[j]) + +-- -- Reset counter when chest is full +-- if (count == #grave_inv) then +-- count = 0 +-- end +-- end +-- end +-- end + +-- -- Clear the player inventory so we don't have duplicate items lying around. +-- -- inv.clear() +-- end + +-- if (grave ~= nil) and (game.players[corpse.character_corpse_player_index] ~= nil)then +-- game.players[corpse.character_corpse_player_index].print("Your corpse got eaten by biters! They kindly dropped your items into a chest! Go get them quick!") +-- end + +-- end + +-- -------------------------------------------------------------------------------- +-- -- Item/Inventory stuff (used in autofill) +-- -------------------------------------------------------------------------------- + +-- -- Transfer Items Between Inventory +-- -- Returns the number of items that were successfully transferred. +-- -- Returns -1 if item not available. +-- -- Returns -2 if can't place item into destInv (ERROR) +-- function TransferItems(srcInv, destEntity, itemStack) +-- -- Check if item is in srcInv +-- if (srcInv.get_item_count(itemStack.name) == 0) then +-- return -1 +-- end + +-- -- Check if can insert into destInv +-- if (not destEntity.can_insert(itemStack)) then +-- return -2 +-- end + +-- -- Insert items +-- local itemsRemoved = srcInv.remove(itemStack) +-- itemStack.count = itemsRemoved +-- return destEntity.insert(itemStack) +-- end + +-- -- Attempts to transfer at least some of one type of item from an array of items. +-- -- Use this to try transferring several items in order +-- -- It returns once it successfully inserts at least some of one type. +-- function TransferItemMultipleTypes(srcInv, destEntity, itemNameArray, itemCount) +-- local ret = 0 +-- for _,itemName in pairs(itemNameArray) do +-- ret = TransferItems(srcInv, destEntity, {name=itemName, count=itemCount}) +-- if (ret > 0) then +-- return ret -- Return the value succesfully transferred +-- end +-- end +-- return ret -- Return the last error code +-- end + +-- -- Autofills a turret with ammo +-- function AutofillTurret(player, turret) +-- local mainInv = player.get_main_inventory() +-- if (mainInv == nil) then return end + +-- -- Attempt to transfer some ammo +-- local ret = TransferItemMultipleTypes(mainInv, turret, {"uranium-rounds-magazine", "piercing-rounds-magazine", "firearm-magazine"}, AUTOFILL_TURRET_AMMO_QUANTITY) + +-- -- Check the result and print the right text to inform the user what happened. +-- if (ret > 0) then +-- -- Inserted ammo successfully +-- -- FlyingText("Inserted ammo x" .. ret, turret.position, my_color_red, player.surface) +-- elseif (ret == -1) then +-- FlyingText("Out of ammo!", turret.position, my_color_red, player.surface) +-- elseif (ret == -2) then +-- FlyingText("Autofill ERROR! - Report this bug!", turret.position, my_color_red, player.surface) +-- end +-- end + +-- -- Autofills a vehicle with fuel, bullets and shells where applicable +-- function AutoFillVehicle(player, vehicle) +-- local mainInv = player.get_main_inventory() +-- if (mainInv == nil) then return end + +-- -- Attempt to transfer some fuel +-- if ((vehicle.name == "car") or (vehicle.name == "tank") or (vehicle.name == "locomotive")) then +-- TransferItemMultipleTypes(mainInv, vehicle, {"nuclear-fuel", "rocket-fuel", "solid-fuel", "coal", "wood"}, 50) +-- end + +-- -- Attempt to transfer some ammo +-- if ((vehicle.name == "car") or (vehicle.name == "tank")) then +-- TransferItemMultipleTypes(mainInv, vehicle, {"uranium-rounds-magazine", "piercing-rounds-magazine", "firearm-magazine"}, 100) +-- end + +-- -- Attempt to transfer some tank shells +-- if (vehicle.name == "tank") then +-- TransferItemMultipleTypes(mainInv, vehicle, {"explosive-uranium-cannon-shell", "uranium-cannon-shell", "explosive-cannon-shell", "cannon-shell"}, 100) +-- end +-- end + +-- -------------------------------------------------------------------------------- +-- -- Resource patch and starting area generation +-- -------------------------------------------------------------------------------- + +---Circle spawn shape (handles land, trees and moat) +---@param surface LuaSurface +---@param centerPos MapPosition +---@param chunkArea BoundingBox +---@param tileRadius number +---@param fillTile string +---@param moat boolean +---@param bridge boolean +---@return nil +function CreateCropCircle(surface, centerPos, chunkArea, tileRadius, fillTile, moat, bridge) + local tile_radius_sqr = tileRadius ^ 2 + + local moat_width = global.ocfg.spawn_general.moat_width_tiles + local moat_radius_sqr = ((tileRadius + moat_width)^2) + + local tree_width = global.ocfg.spawn_general.tree_width_tiles + local tree_radius_sqr_inner = ((tileRadius - 1 - tree_width) ^ 2) -- 1 less to make sure trees are inside the spawn area + local tree_radius_sqr_outer = ((tileRadius - 1) ^ 2) -function DropGravestoneChests(player) - - local grave - local count = 0 - - -- Make sure we save stuff we're holding in our hands. - player.clean_cursor() - - -- Loop through a players different inventories - -- Put it all into a chest. - -- If the chest is full, create a new chest. - for i, id in ipairs{ - defines.inventory.character_armor, - defines.inventory.character_main, - defines.inventory.character_guns, - defines.inventory.character_ammo, - defines.inventory.character_vehicle, - defines.inventory.character_trash} do - - local inv = player.get_inventory(id) - - -- No idea how inv can be nil sometimes...? - if (inv ~= nil) then - if ((#inv > 0) and not inv.is_empty()) then - for j = 1, #inv do - if inv[j].valid_for_read then - - -- Create a chest when counter is reset - if (count == 0) then - grave = DropEmptySteelChest(player) - if (grave == nil) then - -- player.print("Not able to place a chest nearby! Some items lost!") - return - end - grave_inv = grave.get_inventory(defines.inventory.chest) - end - count = count + 1 - - -- Copy the item stack into a chest slot. - grave_inv[count].set_stack(inv[j]) - - -- Reset counter when chest is full - if (count == #grave_inv) then - count = 0 - end - end + local dirtTiles = {} + for i = chunkArea.left_top.x, chunkArea.right_bottom.x, 1 do + for j = chunkArea.left_top.y, chunkArea.right_bottom.y, 1 do + + -- This ( X^2 + Y^2 ) is used to calculate if something is inside a circle area. + -- We avoid using sqrt for performance reasons. + local distSqr = math.floor((centerPos.x - i) ^ 2 + (centerPos.y - j) ^ 2) + + -- Fill in all unexpected water (or force grass) + if (distSqr <= tile_radius_sqr) then + if (surface.get_tile(i, j).collides_with("water-tile") or + global.ocfg.spawn_general.force_grass) then + table.insert(dirtTiles, { name = fillTile, position = { i, j } }) end end - -- Clear the player inventory so we don't have duplicate items lying around. - inv.clear() - end - end - - if (grave ~= nil) then - player.print("Successfully dropped your items into a chest! Go get them quick!") - end -end - --- Dump player items into a chest after the body expires. -function DropGravestoneChestFromCorpse(corpse) - if ((corpse == nil) or (corpse.character_corpse_player_index == nil)) then return end - - local grave, grave_inv - local count = 0 - - local inv = corpse.get_inventory(defines.inventory.character_corpse) - - -- No idea how inv can be nil sometimes...? - if (inv ~= nil) then - if ((#inv > 0) and not inv.is_empty()) then - for j = 1, #inv do - if inv[j].valid_for_read then - - -- Create a chest when counter is reset - if (count == 0) then - grave = DropEmptySteelChest(corpse) - if (grave == nil) then - -- player.print("Not able to place a chest nearby! Some items lost!") - return - end - grave_inv = grave.get_inventory(defines.inventory.chest) - end - count = count + 1 - - -- Copy the item stack into a chest slot. - grave_inv[count].set_stack(inv[j]) + -- Create a tree ring + if ((distSqr < tree_radius_sqr_outer) and (distSqr > tree_radius_sqr_inner)) then + surface.create_entity({ name = "tree-02", amount = 1, position = { i, j } }) + end - -- Reset counter when chest is full - if (count == #grave_inv) then - count = 0 - end + -- Fill moat with water. + if (moat) then + if (bridge and ((j == centerPos.y - 1) or (j == centerPos.y) or (j == centerPos.y + 1))) then + -- This will leave the tiles "as is" on the left and right of the spawn which has the effect of creating + -- land connections if the spawn is on or near land. + elseif ((distSqr < moat_radius_sqr) and (distSqr > tile_radius_sqr)) then + table.insert(dirtTiles, { name = "water", position = { i, j } }) end end end - - -- Clear the player inventory so we don't have duplicate items lying around. - -- inv.clear() - end - - if (grave ~= nil) and (game.players[corpse.character_corpse_player_index] ~= nil)then - game.players[corpse.character_corpse_player_index].print("Your corpse got eaten by biters! They kindly dropped your items into a chest! Go get them quick!") end + surface.set_tiles(dirtTiles) end --------------------------------------------------------------------------------- --- Item/Inventory stuff (used in autofill) --------------------------------------------------------------------------------- - --- Transfer Items Between Inventory --- Returns the number of items that were successfully transferred. --- Returns -1 if item not available. --- Returns -2 if can't place item into destInv (ERROR) -function TransferItems(srcInv, destEntity, itemStack) - -- Check if item is in srcInv - if (srcInv.get_item_count(itemStack.name) == 0) then - return -1 - end - - -- Check if can insert into destInv - if (not destEntity.can_insert(itemStack)) then - return -2 - end - - -- Insert items - local itemsRemoved = srcInv.remove(itemStack) - itemStack.count = itemsRemoved - return destEntity.insert(itemStack) -end - --- Attempts to transfer at least some of one type of item from an array of items. --- Use this to try transferring several items in order --- It returns once it successfully inserts at least some of one type. -function TransferItemMultipleTypes(srcInv, destEntity, itemNameArray, itemCount) - local ret = 0 - for _,itemName in pairs(itemNameArray) do - ret = TransferItems(srcInv, destEntity, {name=itemName, count=itemCount}) - if (ret > 0) then - return ret -- Return the value succesfully transferred - end - end - return ret -- Return the last error code -end - --- Autofills a turret with ammo -function AutofillTurret(player, turret) - local mainInv = player.get_main_inventory() - if (mainInv == nil) then return end - - -- Attempt to transfer some ammo - local ret = TransferItemMultipleTypes(mainInv, turret, {"uranium-rounds-magazine", "piercing-rounds-magazine", "firearm-magazine"}, AUTOFILL_TURRET_AMMO_QUANTITY) - - -- Check the result and print the right text to inform the user what happened. - if (ret > 0) then - -- Inserted ammo successfully - -- FlyingText("Inserted ammo x" .. ret, turret.position, my_color_red, player.surface) - elseif (ret == -1) then - FlyingText("Out of ammo!", turret.position, my_color_red, player.surface) - elseif (ret == -2) then - FlyingText("Autofill ERROR! - Report this bug!", turret.position, my_color_red, player.surface) - end -end - --- Autofills a vehicle with fuel, bullets and shells where applicable -function AutoFillVehicle(player, vehicle) - local mainInv = player.get_main_inventory() - if (mainInv == nil) then return end - - -- Attempt to transfer some fuel - if ((vehicle.name == "car") or (vehicle.name == "tank") or (vehicle.name == "locomotive")) then - TransferItemMultipleTypes(mainInv, vehicle, {"nuclear-fuel", "rocket-fuel", "solid-fuel", "coal", "wood"}, 50) - end - - -- Attempt to transfer some ammo - if ((vehicle.name == "car") or (vehicle.name == "tank")) then - TransferItemMultipleTypes(mainInv, vehicle, {"uranium-rounds-magazine", "piercing-rounds-magazine", "firearm-magazine"}, 100) - end +---` spawn shape (handles land, trees and moat) (Curtesy of jvmguy) +---@param surface LuaSurface +---@param centerPos MapPosition +---@param chunkArea BoundingBox +---@param tileRadius number +---@param fillTile string +---@param moat boolean +---@param bridge boolean +---@return nil +function CreateCropOctagon(surface, centerPos, chunkArea, tileRadius, fillTile, moat, bridge) - -- Attempt to transfer some tank shells - if (vehicle.name == "tank") then - TransferItemMultipleTypes(mainInv, vehicle, {"explosive-uranium-cannon-shell", "uranium-cannon-shell", "explosive-cannon-shell", "cannon-shell"}, 100) - end -end + local moat_width = global.ocfg.spawn_general.moat_width_tiles + local moat_width_outer = tileRadius + moat_width --------------------------------------------------------------------------------- --- Resource patch and starting area generation --------------------------------------------------------------------------------- + local tree_width = global.ocfg.spawn_general.tree_width_tiles + local tree_distance_inner = tileRadius - tree_width --- Enforce a circle of land, also adds trees in a ring around the area. -function CreateCropCircle(surface, centerPos, chunkArea, tileRadius, fillTile) + local dirtTiles = {} + for i = chunkArea.left_top.x, chunkArea.right_bottom.x, 1 do + for j = chunkArea.left_top.y, chunkArea.right_bottom.y, 1 do - local tileRadSqr = tileRadius^2 + local distVar1 = math.floor(math.max(math.abs(centerPos.x - i), math.abs(centerPos.y - j))) + local distVar2 = math.floor(math.abs(centerPos.x - i) + math.abs(centerPos.y - j)) + local distVar = math.max(distVar1, distVar2 * 0.707); - local dirtTiles = {} - for i=chunkArea.left_top.x,chunkArea.right_bottom.x,1 do - for j=chunkArea.left_top.y,chunkArea.right_bottom.y,1 do - - -- This ( X^2 + Y^2 ) is used to calculate if something - -- is inside a circle area. - local distVar = math.floor((centerPos.x - i)^2 + (centerPos.y - j)^2) - - -- Fill in all unexpected water in a circle - if (distVar < tileRadSqr) then - if (surface.get_tile(i,j).collides_with("water-tile") or - global.ocfg.spawn_config.gen_settings.force_grass or - (game.active_mods["oarc-restricted-build"])) then - table.insert(dirtTiles, {name = fillTile, position ={i,j}}) + -- Fill in all unexpected water (or force grass) + if (distVar <= tileRadius) then + if (surface.get_tile(i, j).collides_with("water-tile") or + global.ocfg.spawn_general.force_grass) then + table.insert(dirtTiles, { name = fillTile, position = { i, j } }) end end - -- Create a circle of trees around the spawn point. - if ((distVar < tileRadSqr-100) and - (distVar > tileRadSqr-500)) then - surface.create_entity({name="tree-02", amount=1, position={i, j}}) + -- Create a tree ring + if ((distVar < tileRadius) and (distVar >= tree_distance_inner)) then + surface.create_entity({ name = "tree-01", amount = 1, position = { i, j } }) + end + + -- Fill moat with water + if (moat) then + if (bridge and ((j == centerPos.y - 1) or (j == centerPos.y) or (j == centerPos.y + 1))) then + -- This will leave the tiles "as is" on the left and right of the spawn which has the effect of creating + -- land connections if the spawn is on or near land. + elseif ((distVar > tileRadius) and (distVar <= moat_width_outer)) then + table.insert(dirtTiles, { name = "water", position = { i, j } }) + end end end end - surface.set_tiles(dirtTiles) end --- COPIED FROM jvmguy! --- Enforce a square of land, with a tree border --- this is equivalent to the CreateCropCircle code -function CreateCropOctagon(surface, centerPos, chunkArea, tileRadius, fillTile) +---Square spawn shape (handles land, trees and moat) +---@param surface LuaSurface +---@param centerPos MapPosition +---@param chunkArea BoundingBox +---@param tileRadius number +---@param fillTile string +---@param moat boolean +---@param bridge boolean +---@return nil +function CreateCropSquare(surface, centerPos, chunkArea, tileRadius, fillTile, moat, bridge) + + local moat_width = global.ocfg.spawn_general.moat_width_tiles + local moat_width_outer = tileRadius + moat_width + + local tree_width = global.ocfg.spawn_general.tree_width_tiles + local tree_distance_inner = tileRadius - tree_width local dirtTiles = {} - for i=chunkArea.left_top.x,chunkArea.right_bottom.x,1 do - for j=chunkArea.left_top.y,chunkArea.right_bottom.y,1 do + for i = chunkArea.left_top.x, chunkArea.right_bottom.x, 1 do + for j = chunkArea.left_top.y, chunkArea.right_bottom.y, 1 do - local distVar1 = math.floor(math.max(math.abs(centerPos.x - i), math.abs(centerPos.y - j))) - local distVar2 = math.floor(math.abs(centerPos.x - i) + math.abs(centerPos.y - j)) - local distVar = math.max(distVar1*1.1, distVar2 * 0.707*1.1); - - -- Fill in all unexpected water in a circle - if (distVar < tileRadius+2) then - if (surface.get_tile(i,j).collides_with("water-tile") or - global.ocfg.spawn_config.gen_settings.force_grass or - (game.active_mods["oarc-restricted-build"])) then - table.insert(dirtTiles, {name = fillTile, position ={i,j}}) + -- Max distance from center (either x or y) + local max_distance = math.max(math.abs(centerPos.x - i), math.abs(centerPos.y - j)) + + -- Fill in all unexpected water (or force grass) + if (max_distance <= tileRadius) then + if (surface.get_tile(i, j).collides_with("water-tile") or + global.ocfg.spawn_general.force_grass) then + table.insert(dirtTiles, { name = fillTile, position = { i, j } }) end end -- Create a tree ring - if ((distVar < tileRadius) and - (distVar > tileRadius-2)) then - surface.create_entity({name="tree-01", amount=1, position={i, j}}) + if ((max_distance < tileRadius) and (max_distance >= tree_distance_inner)) then + surface.create_entity({ name = "tree-02", amount = 1, position = { i, j } }) + end + + -- Fill moat with water + if (moat) then + if (bridge and ((j == centerPos.y - 1) or (j == centerPos.y) or (j == centerPos.y + 1))) then + -- This will leave the tiles "as is" on the left and right of the spawn which has the effect of creating + -- land connections if the spawn is on or near land. + elseif ((max_distance > tileRadius) and (max_distance <= moat_width_outer)) then + table.insert(dirtTiles, { name = "water", position = { i, j } }) + end end end end + surface.set_tiles(dirtTiles) end --- Add a circle of water -function CreateMoat(surface, centerPos, chunkArea, tileRadius, moatTile, bridge) - - local tileRadSqr = tileRadius^2 +---Add a circle of water +---@param surface LuaSurface +---@param centerPos MapPosition +---@param chunkArea BoundingBox +---@param tileRadius number +---@param moatTile string +---@param bridge boolean +---@param shape SpawnShapeChoice +---@return nil +function CreateMoat(surface, centerPos, chunkArea, tileRadius, moatTile, bridge, shape) + local tileRadSqr = tileRadius ^ 2 local tiles = {} - for i=chunkArea.left_top.x,chunkArea.right_bottom.x,1 do - for j=chunkArea.left_top.y,chunkArea.right_bottom.y,1 do - - if (bridge and ((j == centerPos.y-1) or (j == centerPos.y) or (j == centerPos.y+1))) then + for i = chunkArea.left_top.x, chunkArea.right_bottom.x, 1 do + for j = chunkArea.left_top.y, chunkArea.right_bottom.y, 1 do + if (bridge and ((j == centerPos.y - 1) or (j == centerPos.y) or (j == centerPos.y + 1))) then -- This will leave the tiles "as is" on the left and right of the spawn which has the effect of creating -- land connections if the spawn is on or near land. else - -- This ( X^2 + Y^2 ) is used to calculate if something -- is inside a circle area. - local distVar = math.floor((centerPos.x - i)^2 + (centerPos.y - j)^2) + local distVar = math.floor((centerPos.x - i) ^ 2 + (centerPos.y - j) ^ 2) -- Create a circle of water - if ((distVar < tileRadSqr+(1500*global.ocfg.spawn_config.gen_settings.moat_size_modifier)) and - (distVar > tileRadSqr)) then - table.insert(tiles, {name = moatTile, position ={i,j}}) + if ((distVar < tileRadSqr + (1500 * global.ocfg.spawn_general.moat_width_tiles)) and + (distVar > tileRadSqr)) then + table.insert(tiles, { name = moatTile, position = { i, j } }) end end end @@ -1256,107 +1505,111 @@ function CreateMoat(surface, centerPos, chunkArea, tileRadius, moatTile, bridge) end -- Create a horizontal line of water +---@param surface LuaSurface +---@param leftPos TilePosition +---@param length integer function CreateWaterStrip(surface, leftPos, length) local waterTiles = {} - for i=0,length,1 do - table.insert(waterTiles, {name = "water", position={leftPos.x+i,leftPos.y}}) + for i = 0, length-1, 1 do + table.insert(waterTiles, { name = "water", position = { leftPos.x + i, leftPos.y } }) end surface.set_tiles(waterTiles) end --- Function to generate a resource patch, of a certain size/amount at a pos. -function GenerateResourcePatch(surface, resourceName, diameter, pos, amount) - local midPoint = math.floor(diameter/2) +--- Function to generate a resource patch, of a certain size/amount at a pos. +---@param surface LuaSurface +---@param resourceName string +---@param diameter integer +---@param position TilePosition +---@param amount integer +function GenerateResourcePatch(surface, resourceName, diameter, position, amount) + local midPoint = math.floor(diameter / 2) if (diameter == 0) then return end - for y=-midPoint, midPoint do - for x=-midPoint, midPoint do - if (not global.ocfg.spawn_config.gen_settings.resources_circle_shape or ((x)^2 + (y)^2 < midPoint^2)) then - surface.create_entity({name=resourceName, amount=amount, - position={pos.x+x, pos.y+y}}) - end - end - end -end - - - - --------------------------------------------------------------------------------- --- Holding pen for new players joining the map --------------------------------------------------------------------------------- -function CreateWall(surface, pos) - local wall = surface.create_entity({name="stone-wall", position=pos, force=MAIN_TEAM}) - if wall then - wall.destructible = false - wall.minable = false - end -end -function CreateHoldingPen(surface, chunkArea) - local radiusTiles = global.ocfg.spawn_config.gen_settings.land_area_tiles-10 - if (((chunkArea.left_top.x >= -(radiusTiles+2*CHUNK_SIZE)) and (chunkArea.left_top.x <= (radiusTiles+2*CHUNK_SIZE))) and - ((chunkArea.left_top.y >= -(radiusTiles+2*CHUNK_SIZE)) and (chunkArea.left_top.y <= (radiusTiles+2*CHUNK_SIZE)))) then - - -- Remove stuff - RemoveAliensInArea(surface, chunkArea) - RemoveInArea(surface, chunkArea, "tree") - RemoveInArea(surface, chunkArea, "resource") - RemoveInArea(surface, chunkArea, "cliff") - - CreateCropCircle(surface, {x=0,y=0}, chunkArea, radiusTiles, "landfill") - CreateMoat(surface, {x=0,y=0}, chunkArea, radiusTiles, "water", false) - CreateMoat(surface, {x=0,y=0}, chunkArea, radiusTiles+10, "out-of-map", false) - CreateMoat(surface, {x=0,y=0}, chunkArea, 2, "out-of-map", false) - end -end + -- Right now only 2 shapes are supported. Circle and Square. + local square_shape = (global.ocfg.spawn_general.resources_shape == RESOURCES_SHAPE_CHOICE_SQUARE) --------------------------------------------------------------------------------- --- EVENT SPECIFIC FUNCTIONS --------------------------------------------------------------------------------- + for y = -midPoint, midPoint do + for x = -midPoint, midPoint do --- Display messages to a user everytime they join -function PlayerJoinedMessages(event) - local player = game.players[event.player_index] - player.print(global.ocfg.welcome_msg) - if (global.oarc_announcements) then - player.print(global.oarc_announcements) + -- Either it's a square, or it's a circle so we check if it's inside the circle. + if (square_shape or ((x) ^ 2 + (y) ^ 2 < midPoint ^ 2)) then + surface.create_entity({ + name = resourceName, + amount = amount, + position = { position.x + x, position.y + y } + }) + end + end end end --- Remove decor to save on file size -function UndecorateOnChunkGenerate(event) - local surface = event.surface - local chunkArea = event.area - RemoveDecorationsArea(surface, chunkArea) - -- If you care to, you can remove all fish with the Undecorator option here: - -- RemoveFish(surface, chunkArea) -end - --- Give player items on respawn --- Intended to be the default behavior when not using separate spawns -function PlayerRespawnItems(event) - GivePlayerItems(game.players[event.player_index]) -end - -function PlayerSpawnItems(event) - GivePlayerStarterItems(game.players[event.player_index]) -end - --- Autofill softmod -function Autofill(event) - local player = game.players[event.player_index] - local eventEntity = event.created_entity - - -- Make sure player isn't dead? - if (player.character == nil) then return end - - if (eventEntity.name == "gun-turret") then - AutofillTurret(player, eventEntity) - end - - if ((eventEntity.name == "car") or (eventEntity.name == "tank") or (eventEntity.name == "locomotive")) then - AutoFillVehicle(player, eventEntity) - end -end +-- -------------------------------------------------------------------------------- +-- -- Holding pen for new players joining the map +-- -------------------------------------------------------------------------------- +-- function CreateWall(surface, pos) +-- local wall = surface.create_entity({name="stone-wall", position=pos, force=MAIN_TEAM}) +-- if wall then +-- wall.destructible = false +-- wall.minable = false +-- end +-- end + +-- function CreateHoldingPen(surface, chunkArea) +-- local radiusTiles = global.ocfg.spawn_config.general.spawn_radius_tiles-10 +-- if (((chunkArea.left_top.x >= -(radiusTiles+2*CHUNK_SIZE)) and (chunkArea.left_top.x <= (radiusTiles+2*CHUNK_SIZE))) and +-- ((chunkArea.left_top.y >= -(radiusTiles+2*CHUNK_SIZE)) and (chunkArea.left_top.y <= (radiusTiles+2*CHUNK_SIZE)))) then + +-- -- Remove stuff +-- RemoveAliensInArea(surface, chunkArea) +-- RemoveInArea(surface, chunkArea, "tree") +-- RemoveInArea(surface, chunkArea, "resource") +-- RemoveInArea(surface, chunkArea, "cliff") + +-- CreateCropCircle(surface, {x=0,y=0}, chunkArea, radiusTiles, "landfill") +-- CreateMoat(surface, {x=0,y=0}, chunkArea, radiusTiles, "water", false) +-- CreateMoat(surface, {x=0,y=0}, chunkArea, radiusTiles+10, "out-of-map", false) +-- CreateMoat(surface, {x=0,y=0}, chunkArea, 2, "out-of-map", false) +-- end +-- end + +-- -------------------------------------------------------------------------------- +-- -- EVENT SPECIFIC FUNCTIONS +-- -------------------------------------------------------------------------------- + +-- -- Display messages to a user everytime they join +-- function PlayerJoinedMessages(event) +-- local player = game.players[event.player_index] +-- player.print(global.ocfg.welcome_msg) +-- if (global.oarc_announcements) then +-- player.print(global.oarc_announcements) +-- end +-- end + +-- -- Remove decor to save on file size +-- function UndecorateOnChunkGenerate(event) +-- local surface = event.surface +-- local chunkArea = event.area +-- RemoveDecorationsArea(surface, chunkArea) +-- -- If you care to, you can remove all fish with the Undecorator option here: +-- -- RemoveFish(surface, chunkArea) +-- end + +-- -- Autofill softmod +-- function Autofill(event) +-- local player = game.players[event.player_index] +-- local eventEntity = event.created_entity + +-- -- Make sure player isn't dead? +-- if (player.character == nil) then return end + +-- if (eventEntity.name == "gun-turret") then +-- AutofillTurret(player, eventEntity) +-- end + +-- if ((eventEntity.name == "car") or (eventEntity.name == "tank") or (eventEntity.name == "locomotive")) then +-- AutoFillVehicle(player, eventEntity) +-- end +-- end diff --git a/lib/offline_protection.lua b/lib/offline_protection.lua new file mode 100644 index 0000000..a303c41 --- /dev/null +++ b/lib/offline_protection.lua @@ -0,0 +1,130 @@ +-- This attempt will try to intercept normal vanilla enemy groups and modify them based on player activity. + +-- Basic logic: +-- on_unit_group_finished_gathering we check what command is given. +-- find destination position +-- check for closest "player" using find_nearest_enemy function +-- if a player is found, check if player is part of a shared spawn +-- Remove the enemy group if no player in the shared spawn is online. + +-- Generic Utility Includes +require("lib/oarc_utils") + +---This function is called when a unit group finishes gathering. +---@param event EventData.on_unit_group_finished_gathering +---@return nil +function OarcModifyEnemyGroup(event) + + local group = event.group + + -- Check validity + if ((group == nil) or (group.command == nil) or not TableContains(ENEMY_FORCES_NAMES, group.force.name)) then + log("OarcModifyEnemyGroup ignoring INVALID group/command") + return + end + + -- Make sure the attack is of a TYPE that we care about. + if ((group.command.type == defines.command.attack_area) or + (group.command.type == defines.command.build_base)) then + -- log("OarcModifyEnemyGroup MODIFYING command TYPE=" .. group.command.type) + else + -- log("OarcModifyEnemyGroup ignoring command TYPE=" .. group.command.type) + return + end + + -- (group.command.type == defines.command.attack) or + -- defines.command.attack --> target --> target.position + -- if (group.command.type == defines.command.attack) then + -- log("OarcModifyEnemyGroup defines.command.attack NOT IMPLEMENTED YET!") + -- return + -- end + + -- defines.command.attack_area --> destination --> closest enemy (within 3 chunk radius?) + -- defines.command.build_base --> destination --> closest enemy (expansion chunk distance?) + + local destination = group.command.destination + + local search_radius = CHUNK_SIZE*3 + if (group.command.type == defines.command.build_base) then + search_radius = CHUNK_SIZE * (game.map_settings.enemy_expansion.max_expansion_distance) + end + + -- Look for any player force targets near the destination point. + local target_entities = group.surface.find_entities_filtered{ + position=destination, + radius=search_radius, + force=ENEMY_FORCES_NAMES_INCL_NEUTRAL, + limit=50, + invert=true} + + -- Search through them all to find anything with a non-nil last_user. + local target_entity = nil + for _,target in ipairs(target_entities) do + if (target.last_user ~= nil) then + target_entity = target + break + end + end + + -- No targets found with a last_user + if (target_entity == nil) then + + -- This is unexpected, not sure under which conditions this would happen. + if (group.command.type == defines.command.attack_area) then + -- SendBroadcastMsg("OarcModifyEnemyGroup find_nearest_enemy attack_area FAILED!?!? " .. GetGPStext(group.surface.name, group.position) .. " Target: " .. GetGPStext(group.surface.name, group.command.destination)) + log("ERROR - OarcModifyEnemyGroup find_nearest_enemy attack_area FAILED!?!?") + -- for _,member in pairs(group.members) do + -- member.destroy() + -- end + + -- This is fine, as the enemy group is just expanding / building bases + else + -- log("OarcModifyEnemyGroup find_nearest_enemy did not find anything!") + end + return + end + + -- Most common target will be a built entity with a "last_user" + local target_player = target_entity.last_user + + -- -- Target could also be a player character (more rare) + -- if (target_player == nil) and (target_entity.type == "character") then + -- target_player = target_entity.player + -- end + + -- I don't think this should happen ever... + if ((target_player == nil) or (not target_player.valid)) then + -- SendBroadcastMsg("ERROR?? target_player nil/invalid " .. GetGPStext(group.surface.name, group.position) .. " Target: " .. GetGPStext(group.surface.name, target_entity.position)) + log("ERROR - OarcModifyEnemyGroup target_player nil/invalid?") + -- for _,member in pairs(group.members) do + -- member.destroy() + -- end + return + end + + -- Is the target player online? Then the attack can go through. + if (target_player.connected) then + -- SendBroadcastMsg("Enemy group released (player): " .. GetGPStext(group.surface.name, group.position) .. " Target: " .. GetGPStext(group.surface.name, target_entity.position) .. " " .. target_player.name) + -- log("OarcModifyEnemyGroup RELEASING enemy group since player is ONLINE " .. target_player.name) + return + end + + -- Find the shared spawn that the player is part of. + -- This could be the own player's spawn (quite likely) + local online_players = GetPlayersFromSameSpawn(target_player.name, false) + + -- Is someone in the group online? + if (#online_players > 0) then + -- SendBroadcastMsg("Enemy group released (shared): " .. GetGPStext(group.surface.name, group.position) .. " Target: " .. GetGPStext(group.surface.name, target_entity.position) .. " " .. target_player.name) + -- log("OarcModifyEnemyGroup RELEASING enemy group since someone in the group is ONLINE " .. target_player.name) + return + end + + -- Otherwise, we delete the group. + for _,member in pairs(group.members) do + member.destroy() + end + -- SendBroadcastMsg("Enemy group deleted: " .. GetGPStext(group.surface.name, group.position) .. " Target: " .. GetGPStext(group.surface.name, target_entity.position) .. " " .. target_player.name) + log("OarcModifyEnemyGroup REMOVED enemy group since nobody was online? " .. target_player.name) + +end \ No newline at end of file diff --git a/lib/player_list.lua b/lib/player_list.lua deleted file mode 100644 index 8f99fc3..0000000 --- a/lib/player_list.lua +++ /dev/null @@ -1,36 +0,0 @@ --- oarc_player_list.lua --- Mar 2019 - --------------------------------------------------------------------------------- --- Player List GUI - My own version --------------------------------------------------------------------------------- -function CreatePlayerListGuiTab(tab_container, player) - local scrollFrame = tab_container.add{type="scroll-pane", - name="playerList-panel", - direction = "vertical"} - ApplyStyle(scrollFrame, my_player_list_fixed_width_style) - scrollFrame.horizontal_scroll_policy = "never" - - AddLabel(scrollFrame, "online_title_msg", "Online Players:", my_label_header_style) - for _,player in pairs(game.connected_players) do - local caption_str = player.name.." ["..player.force.name.."]".." ("..formattime_hours_mins(player.online_time)..")" - if (player.admin) then - AddLabel(scrollFrame, player.name.."_plist", caption_str, my_player_list_admin_style) - else - AddLabel(scrollFrame, player.name.."_plist", caption_str, my_player_list_style) - end - end - - -- List offline players - if (global.ocfg.list_offline_players) then - AddSpacerLine(scrollFrame) - AddLabel(scrollFrame, "offline_title_msg", "Offline Players:", my_label_header_grey_style) - for _,player in pairs(game.players) do - if (not player.connected) then - local caption_str = player.name.." ["..player.force.name.."]".." ("..formattime_hours_mins(player.online_time)..")" - local text = scrollFrame.add{type="label", caption=caption_str, name=player.name.."_plist"} - ApplyStyle(text, my_player_list_offline_style) - end - end - end -end diff --git a/lib/regrowth_map.lua b/lib/regrowth_map.lua index b37a930..e82d078 100644 --- a/lib/regrowth_map.lua +++ b/lib/regrowth_map.lua @@ -1,7 +1,3 @@ --- regrowth_map.lua --- Sep 2019 --- REVERTED BACK TO SOFT MOD - -- Code tracks all chunks generated and allows for deleting of inactive chunks. -- -- Basic rules of regrowth: @@ -13,241 +9,506 @@ -- the on_sector_scanned event. -- 5. Chunks timeout after 1 hour-ish, configurable -require("lib/oarc_utils") -require("config") +--- These chunks are marked for removal. They will be deleted by the regrowth system. +--- If it gets refreshed before it is removed, then it will be marked safe again +REGROWTH_FLAG_REMOVAL = -1 + +--- If a chunk is marked "active", then it will only be checked by the "world eater" system if that is enabled. +--- World eater does more extensive checks to see if a chunk might be safe to delete. For example, if a player builds +--- stuff in a chunk it will be marked as "active" and won't be checked by the regrowth system. +REGROWTH_FLAG_ACTIVE = -2 + +--- These chunks will NEVER be deleted by the regrowth + world eater systems. However, they can be overwritten in some +--- cases. Like when a player leaves the game early and their spawn is deleted. +REGROWTH_FLAG_PERMANENT = -3 + + -REGROWTH_TIMEOUT_TICKS = TICKS_PER_HOUR -- TICKS_PER_HOUR TICKS_PER_MINUTE +--- Radius in chunks around a player to mark as safe. +REGROWTH_ACTIVE_AREA_AROUND_PLAYER = 2 --- Init globals and set player join area to be off limits. +---The removal list contains chunks that are marked for removal. Each entry is a table with the following fields: +---@alias RemovalListEntry { pos : ChunkPosition, force: boolean, surface: string } + + +---Init globals for regrowth +---@return nil function RegrowthInit() global.rg = {} global.rg.player_refresh_index = nil - global.rg.force_removal_flag = -2000 - global.rg.map = {} + + global.rg.force_removal_flag = -2000 -- Set to a negative number to disable it by default + + global.rg.current_surface = nil -- The current surface we are iterating through + global.rg.current_surface_index = 1 + global.rg.active_surfaces = {} -- List of all surfaces with regrowth enabled + global.rg.chunk_iter = nil -- We only iterate through onface at a time + + global.rg.we_chunk_iter = nil + global.rg.we_current_surface = nil + global.rg.we_current_surface_index = 1 + + + ---@type LuaEntity[] + global.rg.spidertrons = {} -- List of all spidertrons in the game + global.rg_spidertron_index = 1 + + ---@type RemovalListEntry[] global.rg.removal_list = {} - global.rg.chunk_iter = nil - global.rg.world_eater_iter = nil - global.rg.timeout_ticks = REGROWTH_TIMEOUT_TICKS + + for surface_name,_ in pairs(game.surfaces) do + InitSurface(surface_name --[[@as string]]) + end end -function TriggerCleanup() - global.rg.force_removal_flag = game.tick +---Called when a new surface is created. This is used to add new surfaces to the regrowth map. +---@param event EventData.on_surface_created +---@return nil +function RegrowthSurfaceCreated(event) + InitSurface(game.surfaces[event.surface_index].name) end -function RegrowthForceRemoveChunksCmd(cmd_table) - if (game.players[cmd_table.player_index].admin) then - TriggerCleanup() +---Called when a surface is deleted. This is used to remove surfaces from the regrowth map. +---@param event EventData.on_pre_surface_deleted +---@return nil +function RegrowthSurfaceDeleted(event) + log("WARNING - RegrowthSurfaceDeleted: " .. game.surfaces[event.surface_index].name) + local surface_name = game.surfaces[event.surface_index].name + RegrowthDisableSurface(surface_name ) +end + +---Initialize the new surface for regrowth +---@param surface_name string - The surface name to act on +---@return nil +function InitSurface(surface_name) + + if (not IsSurfaceBlacklisted(surface_name) and not TableContains(global.rg.active_surfaces, surface_name)) then + log("Adding surface to regrowth: " .. surface_name) + + -- Add a new surface to the regrowth map (Don't overwrite if it already exists) + if (global.rg[surface_name] == nil) then + global.rg[surface_name] = {} + end + + -- This is a 2D array of chunk positions and their last tick updated / status (Don't overwrite if it already exists) + if (global.rg[surface_name].map == nil) then + global.rg[surface_name].map = {} + end + + -- Set the current surface to the first one found if none are set. + if (global.rg.current_surface == nil) then + global.rg.current_surface = surface_name + global.rg.we_current_surface = surface_name + end + + global.rg[surface_name].active = true + table.insert(global.rg.active_surfaces, surface_name) + end +end + +function RegrowthDisableSurface(surface_name) + + -- We don't want to delete the surface history in case it's re-enabled later! + -- global.rg[surface_name] = nil + + global.rg[surface_name].active = false + TableRemoveOneUsingPairs(global.rg.active_surfaces, surface_name) + + -- Make sure indices are reset if needed + if (global.rg.current_surface == surface_name) then + global.rg.current_surface = nil + global.rg.current_surface_index = 1 + end + if (global.rg.we_current_surface == surface_name) then + global.rg.we_current_surface = nil + global.rg.we_current_surface_index = 1 + end + if #global.rg.active_surfaces > 0 then + global.rg.current_surface = global.rg.active_surfaces[1] + global.rg.we_current_surface = global.rg.active_surfaces[1] end end --- Get the next player index available -function GetNextPlayerIndex(player_index) - if (not global.rg.player_refresh_index or not game.players[global.rg.player_refresh_index]) then +---Simple check to see if a surface is enabled for regrowth +---@param surface_name string - The surface name to act on +---@return boolean +function IsRegrowthEnabledOnSurface(surface_name) + if (global.rg[surface_name] == nil) then return false end + return global.rg[surface_name].active +end + +function RegrowthEnableSurface(surface_name) + InitSurface(surface_name) +end + +---Trigger an immediate cleanup of any chunks that are marked for removal. +---@return nil +function TriggerCleanup() + global.rg.force_removal_flag = game.tick +end + +-- Turn this into a admin GUI button. +-- function RegrowthForceRemoveChunksCmd(cmd_table) +-- if (game.players[cmd_table.player_index].admin) then +-- TriggerCleanup() +-- end +-- end + +---Get the next player index available. This is used to loop through ONLINE players to refresh the areas around them. +---@return integer +function GetNextConnectedPlayerIndex() + if (global.rg.player_refresh_index == nil) or (game.connected_players[global.rg.player_refresh_index] == nil) then global.rg.player_refresh_index = 1 else global.rg.player_refresh_index = global.rg.player_refresh_index + 1 end - if (global.rg.player_refresh_index > #game.players) then + if (global.rg.player_refresh_index > #game.connected_players) then global.rg.player_refresh_index = 1 end return global.rg.player_refresh_index end --- Adds new chunks to the global table to track them. --- This should always be called first in the chunk generate sequence --- (Compared to other RSO & Oarc related functions...) +---@alias ActiveSurfaceInfo { surface : string, index : integer } + +---Sets the current surface to the next active surface. This is used to loop through surfaces. +---@param current_index integer - The current index in the active surfaces list +---@return ActiveSurfaceInfo - The new current surface name and index +function GetNextActiveSurface(current_index) + + local count = #(global.rg.active_surfaces) + local next_index = current_index + 1 + + if (next_index > count) then + next_index = 1 + end + + local next_surface = global.rg.active_surfaces[next_index] + + return { surface = next_surface, index = next_index } +end + +---Adds new chunks to the global table to track them. +---This should always be called first in the chunk generate sequence +---(Compared to other RSO & Oarc related functions...) +---@param event EventData.on_chunk_generated +---@return nil function RegrowthChunkGenerate(event) - local c_pos = GetChunkPosFromTilePos(event.area.left_top) + local c_pos = event.position + local surface_name = event.surface.name - -- Surface must be "added" first. - if (global.rg == nil) then return end + -- Surface not init or not active, ignore it. + if not IsRegrowthEnabledOnSurface(surface_name) then return end -- If this is the first chunk in that row: - if (global.rg.map[c_pos.x] == nil) then - global.rg.map[c_pos.x] = {} + if (global.rg[surface_name].map[c_pos.x] == nil) then + global.rg[surface_name].map[c_pos.x] = {} end -- Only update it if it isn't already set! - if (global.rg.map[c_pos.x][c_pos.y] == nil) then - global.rg.map[c_pos.x][c_pos.y] = game.tick + if (global.rg[surface_name].map[c_pos.x][c_pos.y] == nil) then + global.rg[surface_name].map[c_pos.x][c_pos.y] = game.tick + -- log("RegrowthChunkGenerate: " .. c_pos.x .. "," .. c_pos.y .. " on surface: " .. surface_name) end end --- Mark an area for "immediate" forced removal -function RegrowthMarkAreaForRemoval(pos, chunk_radius) +---Mark an area for "immediate" forced removal, this will override any pemranent flags. +---@param surface_name string - The surface name to act on +---@param pos TilePosition - The tile position to mark for removal +---@param chunk_radius integer - The radius in chunks around the position to mark for removal +---@return nil +function RegrowthMarkAreaForRemoval(surface_name, pos, chunk_radius) local c_pos = GetChunkPosFromTilePos(pos) - for i=-chunk_radius,chunk_radius do - local x = c_pos.x+i - for k=-chunk_radius,chunk_radius do - local y = c_pos.y+k + for i = -chunk_radius, chunk_radius do + local x = c_pos.x + i + for k = -chunk_radius, chunk_radius do + local y = c_pos.y + k - if (global.rg.map[x] ~= nil) then - global.rg.map[x][y] = nil + if (global.rg[surface_name].map[x] == nil) then + global.rg[surface_name].map[x] = {} end - table.insert(global.rg.removal_list, {pos={x=x,y=y},force=true}) - end - if (table_size(global.rg.map[x]) == 0) then - global.rg.map[x] = nil - end - end -end + global.rg[surface_name].map[x][y] = REGROWTH_FLAG_REMOVAL --- Downgrades permanent flag to semi-permanent. -function RegrowthMarkAreaNotPermanentOVERWRITE(pos, chunk_radius) - local c_pos = GetChunkPosFromTilePos(pos) - for i=-chunk_radius,chunk_radius do - local x = c_pos.x+i - for k=-chunk_radius,chunk_radius do - local y = c_pos.y+k - - if (global.rg.map[x] and global.rg.map[x][y] and (global.rg.map[x][y] == -2)) then - global.rg.map[x][y] = -1 - end + ---@type RemovalListEntry + local removal_entry = { pos = { x = x, y = y }, force = true, surface = surface_name } + table.insert(global.rg.removal_list, removal_entry) end end end --- Marks a chunk containing a position to be relatively permanent. -function MarkChunkSafe(c_pos, permanent) - if (global.rg.map[c_pos.x] == nil) then - global.rg.map[c_pos.x] = {} +-- ---Downgrades permanent flag to semi-permanent. +-- ---@param surface_name string - The surface name to act on +-- ---@param pos TilePosition - The tile position to mark +-- ---@param chunk_radius integer - The radius in chunks around the position to mark +-- ---@return nil +-- function RegrowthMarkAreaNotPermanentOVERWRITE(surface_name, pos, chunk_radius) +-- local c_pos = GetChunkPosFromTilePos(pos) +-- for i = -chunk_radius, chunk_radius do +-- local x = c_pos.x + i +-- for k = -chunk_radius, chunk_radius do +-- local y = c_pos.y + k + +-- if (global.rg[surface_name].map[x] and +-- global.rg[surface_name].map[x][y] and +-- (global.rg[surface_name].map[x][y] == REGROWTH_FLAG_PERMANENT)) then +-- global.rg[surface_name].map[x][y] = REGROWTH_FLAG_ACTIVE +-- end +-- end +-- end +-- end + +---Marks a chunk containing a position to be relatively permanent. +---@param surface_name string - The surface name to act on +---@param c_pos ChunkPosition - The chunk position to mark +---@param permanent boolean - If true, the chunk will be marked as permanent +---@return nil +function MarkChunkSafe(surface_name, c_pos, permanent) + if (global.rg[surface_name].map[c_pos.x] == nil) then + global.rg[surface_name].map[c_pos.x] = {} end if (permanent) then - global.rg.map[c_pos.x][c_pos.y] = -2 + global.rg[surface_name].map[c_pos.x][c_pos.y] = REGROWTH_FLAG_PERMANENT - -- Make sure we don't overwrite... - elseif (global.rg.map[c_pos.x][c_pos.y] and (global.rg.map[c_pos.x][c_pos.y] ~= -2)) then - global.rg.map[c_pos.x][c_pos.y] = -1 + -- Make sure we don't overwrite unless it's a permanent flag + elseif (global.rg[surface_name].map[c_pos.x][c_pos.y] and + (global.rg[surface_name].map[c_pos.x][c_pos.y] ~= REGROWTH_FLAG_PERMANENT)) then + global.rg[surface_name].map[c_pos.x][c_pos.y] = REGROWTH_FLAG_ACTIVE end end --- Marks a safe area around a CHUNK position to be relatively permanent. -function RegrowthMarkAreaSafeGivenChunkPos(c_pos, chunk_radius, permanent) - if (global.rg == nil) then return end - - for i=-chunk_radius,chunk_radius do - for j=-chunk_radius,chunk_radius do - MarkChunkSafe({x=c_pos.x+i,y=c_pos.y+j}, permanent) +---Marks a safe area around a CHUNK position to be relatively permanent. +---@param surface_name string - The surface name to act on +---@param c_pos ChunkPosition - The chunk position to mark +---@param chunk_radius integer - The radius in chunks around the position to mark +---@param permanent boolean - If true, the chunk will be marked as permanent +---@return nil +function RegrowthMarkAreaSafeGivenChunkPos(surface_name, c_pos, chunk_radius, permanent) + if (global.rg[surface_name] == nil) then return end + + for i = -chunk_radius, chunk_radius do + for j = -chunk_radius, chunk_radius do + MarkChunkSafe(surface_name, { x = c_pos.x + i, y = c_pos.y + j }, permanent) end end end --- Marks a safe area around a TILE position to be relatively permanent. -function RegrowthMarkAreaSafeGivenTilePos(pos, chunk_radius, permanent) - if (global.rg == nil) then return end +---Marks a safe area around a TILE position to be relatively permanent. +---@param surface_name string - The surface name to act on +---@param pos TilePosition - The tile position to mark +---@param chunk_radius integer - The radius in chunks around the position to mark +---@param permanent boolean - If true, the chunk will be marked as permanent +---@return nil +function RegrowthMarkAreaSafeGivenTilePos(surface_name, pos, chunk_radius, permanent) + if not IsRegrowthEnabledOnSurface(surface_name) then return end local c_pos = GetChunkPosFromTilePos(pos) - RegrowthMarkAreaSafeGivenChunkPos(c_pos, chunk_radius, permanent) + RegrowthMarkAreaSafeGivenChunkPos(surface_name, c_pos, chunk_radius, permanent) end --- Refreshes timers on a chunk containing position -function RefreshChunkTimer(pos, bonus_time) +---Refreshes timers on a chunk containing position +---@param surface_name string - The surface name to act on +---@param pos TilePosition - The tile position to mark +---@param bonus_time integer - The bonus time to add to the current game tick +---@return nil +function RefreshChunkTimer(surface_name, pos, bonus_time) local c_pos = GetChunkPosFromTilePos(pos) - if (global.rg.map[c_pos.x] == nil) then - global.rg.map[c_pos.x] = {} + if (global.rg[surface_name].map[c_pos.x] == nil) then + global.rg[surface_name].map[c_pos.x] = {} end - if (global.rg.map[c_pos.x][c_pos.y] >= 0) then - global.rg.map[c_pos.x][c_pos.y] = game.tick + bonus_time + if (global.rg[surface_name].map[c_pos.x][c_pos.y] >= REGROWTH_FLAG_REMOVAL) then + global.rg[surface_name].map[c_pos.x][c_pos.y] = game.tick + bonus_time end end --- Refreshes timers on all chunks around a certain area -function RefreshArea(pos, chunk_radius, bonus_time) + +---Refreshes timers on all chunks around a certain area +---@param surface_name string - The surface name to act on +---@param pos TilePosition - The tile position to mark +---@param chunk_radius integer - The radius in chunks around the position to mark +---@param bonus_time integer - The bonus time to add to the current game tick +---@return nil +function RefreshArea(surface_name, pos, chunk_radius, bonus_time) local c_pos = GetChunkPosFromTilePos(pos) - for i=-chunk_radius,chunk_radius do - local x = c_pos.x+i - for k=-chunk_radius,chunk_radius do - local y = c_pos.y+k + RefreshAreaChunkPosition(surface_name, c_pos, chunk_radius, bonus_time) +end - if (global.rg.map[x] == nil) then - global.rg.map[x] = {} +---Refreshes timers on all chunks around a certain area +---@param surface_name string - The surface name to act on +---@param c_pos ChunkPosition - The chunk position to mark +---@param chunk_radius integer - The radius in chunks around the position to mark +---@param bonus_time integer - The bonus time to add to the current game tick +---@return nil +function RefreshAreaChunkPosition(surface_name, c_pos, chunk_radius, bonus_time) + for i = -chunk_radius, chunk_radius do + local x = c_pos.x + i + for k = -chunk_radius, chunk_radius do + local y = c_pos.y + k + + if (global.rg[surface_name].map[x] == nil) then + global.rg[surface_name].map[x] = {} end - if ((global.rg.map[x][y] == nil) or (global.rg.map[x][y] >= 0)) then - global.rg.map[x][y] = game.tick + bonus_time + if ((global.rg[surface_name].map[x][y] == nil) or (global.rg[surface_name].map[x][y] >= REGROWTH_FLAG_REMOVAL)) then + global.rg[surface_name].map[x][y] = game.tick + bonus_time end end end end --- Refreshes timers on all chunks near an ACTIVE radar +---Refreshes timers on all chunks near an ACTIVE radar +---@param event EventData.on_sector_scanned +---@return nil function RegrowthSectorScan(event) - if (event.radar.surface.name ~= GAME_SURFACE_NAME) then return end + local surface_name = event.radar.surface.name + + -- Surface not in regrowth map, ignore it. + if (global.rg[surface_name] == nil) then return end - RefreshArea(event.radar.position, 14, 0) + ---@type integer + local radar_range = event.radar.prototype.max_distance_of_nearby_sector_revealed --TODO: Space age quality might affect this? + RefreshAreaChunkPosition(surface_name, event.chunk_position, radar_range, 0) end --- Refresh all chunks near a single player. Cyles through all connected players. +---Refresh all chunks near a single player. Cyles through all connected players. +---@return nil function RefreshPlayerArea() - player_index = GetNextPlayerIndex() + player_index = GetNextConnectedPlayerIndex() if (player_index and game.connected_players[player_index]) then local player = game.connected_players[player_index] - + local surface_name = player.surface.name + if (not player.character) then return end - if (player.character.surface.name ~= GAME_SURFACE_NAME) then return end + if (global.rg[surface_name] == nil) or (not global.rg[surface_name].active) then return end - RefreshArea(player.position, 4, 0) + RefreshArea(surface_name, player.position, REGROWTH_ACTIVE_AREA_AROUND_PLAYER, 0) end end --- Gets the next chunk the array map and checks to see if it has timed out. --- Adds it to the removal list if it has. -function RegrowthSingleStepArray() +---Updates the chunk_iter and returns the next chunk from it. May not be valid if it no chunks exist. +---@return ChunkPositionAndArea? +function GetNextChunkAndUpdateIter() -- Make sure we have a valid iterator! if (not global.rg.chunk_iter or not global.rg.chunk_iter.valid) then - global.rg.chunk_iter = game.surfaces[GAME_SURFACE_NAME].get_chunks() + global.rg.chunk_iter = game.surfaces[global.rg.current_surface].get_chunks() end local next_chunk = global.rg.chunk_iter() -- Check if we reached the end - if (not next_chunk) then - global.rg.chunk_iter = game.surfaces[GAME_SURFACE_NAME].get_chunks() + if (next_chunk == nil) then + + -- Switch to the next active surface + local next_surface_info = GetNextActiveSurface(global.rg.current_surface_index) + global.rg.current_surface = next_surface_info.surface + global.rg.current_surface_index = next_surface_info.index + global.rg.chunk_iter = game.surfaces[global.rg.current_surface].get_chunks() next_chunk = global.rg.chunk_iter() end - -- Do we have it in our map? - if (not global.rg.map[next_chunk.x] or not global.rg.map[next_chunk.x][next_chunk.y]) then - return -- Chunk isn't in our map so we don't care? - end + return next_chunk +end - -- If the chunk has timed out, add it to the removal list - local c_timer = global.rg.map[next_chunk.x][next_chunk.y] - if ((c_timer ~= nil) and (c_timer >= 0) and ((c_timer + global.rg.timeout_ticks) < game.tick)) then +---Updates the chunk_iter (for World Eater) and returns the next chunk from it. May not be valid if it no chunks exist. +---@return ChunkPositionAndArea? +function GetNextChunkAndUpdateWorldEaterIter() + -- Make sure we have a valid iterator! + if (not global.rg.we_chunk_iter or not global.rg.we_chunk_iter.valid) then + global.rg.we_chunk_iter = game.surfaces[global.rg.we_current_surface].get_chunks() + end + + local next_chunk = global.rg.we_chunk_iter() + + -- Check if we reached the end + if (next_chunk == nil) then + + -- Switch to the next active surface + local next_surface_info = GetNextActiveSurface(global.rg.we_current_surface_index) + global.rg.we_current_surface = next_surface_info.surface + global.rg.we_current_surface_index = next_surface_info.index + global.rg.we_chunk_iter = game.surfaces[global.rg.we_current_surface].get_chunks() + next_chunk = global.rg.we_chunk_iter() + end + + return next_chunk +end + +---Gets the next chunk the array map and checks to see if it has timed out. +---Adds it to the removal list if it has. +---@return nil +function RegrowthSingleStepArray() + + local next_chunk = GetNextChunkAndUpdateIter() + if (next_chunk == nil) then return end + local current_surface = global.rg.current_surface + + -- It's possible that if regrowth is disabled/enabled during runtime we might miss on_chunk_generated. + -- This will catch that case and add the chunk to the map. + if (global.rg[current_surface].map[next_chunk.x] == nil) then + global.rg[current_surface].map[next_chunk.x] = {} + end + if (global.rg[current_surface].map[next_chunk.x][next_chunk.y] == nil and game.surfaces[current_surface].is_chunk_generated(next_chunk)) then + log("RegrowthSingleStepArray: Chunk not in map: " .. next_chunk.x .. "," .. next_chunk.y .. " on surface: " .. current_surface) + local has_player_entities = CheckIfChunkHasAnyPlayerEntities(current_surface, next_chunk) + if has_player_entities then + global.rg[current_surface].map[next_chunk.x][next_chunk.y] = REGROWTH_FLAG_ACTIVE + else + global.rg[current_surface].map[next_chunk.x][next_chunk.y] = game.tick + end + return + end + + -- If the chunk has timed out, add it to the removal list + local c_timer = global.rg[current_surface].map[next_chunk.x][next_chunk.y] + local interval_ticks = global.ocfg.regrowth.cleanup_interval * TICKS_PER_MINUTE + if ((c_timer ~= nil) and (c_timer >= 0) and ((c_timer + interval_ticks) < game.tick)) then -- Check chunk actually exists - if (game.surfaces[GAME_SURFACE_NAME].is_chunk_generated({x=next_chunk.x, y=next_chunk.y})) then - table.insert(global.rg.removal_list, {pos={x=next_chunk.x, y=next_chunk.y}, force=false}) - global.rg.map[next_chunk.x][next_chunk.y] = nil + if (game.surfaces[current_surface].is_chunk_generated({ x = next_chunk.x, y = next_chunk.y })) then + + ---@type RemovalListEntry + local removal_entry = {pos = {x = next_chunk.x, y = next_chunk.y }, force = false, surface = current_surface} + table.insert(global.rg.removal_list, removal_entry) + global.rg[current_surface].map[next_chunk.x][next_chunk.y] = REGROWTH_FLAG_REMOVAL + else + log("WARN - RegrowthSingleStepArray: Chunk not generated?: " .. next_chunk.x .. "," .. next_chunk.y .. " on surface: " .. current_surface) + global.rg[current_surface].map[next_chunk.x][next_chunk.y] = nil end end end --- Remove all chunks at same time to reduce impact to FPS/UPS +---Remove all chunks at same time to reduce impact to FPS/UPS +---@return nil function OarcRegrowthRemoveAllChunks() - for key,c_remove in pairs(global.rg.removal_list) do + for key, c_remove in pairs(global.rg.removal_list) do local c_pos = c_remove.pos + local surface_name = c_remove.surface - -- Confirm chunk is still expired - if (not global.rg.map[c_pos.x] or not global.rg.map[c_pos.x][c_pos.y]) then + -- Confirm chunk is still marked for removal or is a force removal, if it's nil, something else happened? + if (global.rg[surface_name].map[c_pos.x] ~= nil) then -- If it is FORCE removal, then remove it regardless of pollution. if (c_remove.force) then - game.surfaces[GAME_SURFACE_NAME].delete_chunk(c_pos) + game.surfaces[surface_name].delete_chunk(c_pos) + + elseif (global.rg[surface_name].map[c_pos.x][c_pos.y] == REGROWTH_FLAG_REMOVAL) then - -- If it is a normal timeout removal, don't do it if there is pollution in the chunk. - elseif (game.surfaces[GAME_SURFACE_NAME].get_pollution({c_pos.x*32,c_pos.y*32}) > 0) then - global.rg.map[c_pos.x][c_pos.y] = game.tick + -- If it is a normal timeout removal, don't do it if there is pollution in the chunk. + if (game.surfaces[surface_name].get_pollution({ c_pos.x * 32, c_pos.y * 32 }) > 0) then + global.rg[surface_name].map[c_pos.x][c_pos.y] = game.tick - -- Else delete the chunk - else - game.surfaces[GAME_SURFACE_NAME].delete_chunk(c_pos) + -- Else delete the chunk + else + game.surfaces[surface_name].delete_chunk(c_pos) + global.rg[surface_name].map[c_pos.x][c_pos.y] = nil + end end + else + -- This should never happen, TODO: check if it does? + error("ERROR - OarcRegrowthRemoveAllChunks: Chunk not in map: " .. c_pos.x .. "," .. c_pos.y .. " on surface: " .. surface_name) end -- Remove entry @@ -256,41 +517,46 @@ function OarcRegrowthRemoveAllChunks() -- MUST GET A NEW CHUNK ITERATOR ON DELETE CHUNK! global.rg.chunk_iter = nil - global.rg.world_eater_iter = nil + global.rg.we_chunk_iter = nil end --- This is the main work function, it checks a single chunk in the list --- per tick. It works according to the rules listed in the header of this --- file. +---This is the main work function, it checks a single chunk in the list per tick. It works according to the rules +---listed in the header of this file. +---@return nil function RegrowthOnTick() - -- Every half a second, refresh all chunks near a single player - -- Cyles through all players. Tick is offset by 2 - if ((game.tick % (30)) == 2) then - RefreshPlayerArea() - end + if (#global.rg.active_surfaces > 0) then - -- Every tick, check a few points in the 2d array of the only active surface According to /measured-command this - -- shouldn't take more than 0.1ms on average - for i=1,20 do - RegrowthSingleStepArray() - end + -- Every half a second, refresh all chunks near a single player + -- Cyles through all players. Tick is offset by 2 + if ((game.tick % (30)) == 2) then + RefreshPlayerArea() + end + + -- Refresh a single spidertron every tick + RefreshSpidertronArea() - if (not global.world_eater_disable) then - WorldEaterSingleStep() + -- Every tick, check a few points in the 2d array of the only active surface + for i = 1, 20 do + RegrowthSingleStepArray() + end + + if (global.ocfg.regrowth.enable_world_eater) then + WorldEaterSingleStep() + end end -- Allow enable/disable of auto cleanup, can change during runtime. - local interval_ticks = global.rg.timeout_ticks + local interval_ticks = global.ocfg.regrowth.cleanup_interval * TICKS_PER_MINUTE -- Send a broadcast warning before it happens. - if ((game.tick % interval_ticks) == interval_ticks-(60*30 + 1)) then + if ((game.tick % interval_ticks) == interval_ticks - (60 * 30 + 1)) then if (#global.rg.removal_list > 100) then SendBroadcastMsg("Map cleanup in 30 seconds... Unused and old map chunks will be deleted!") end end -- Delete all listed chunks across all active surfaces - if ((game.tick % interval_ticks) == interval_ticks-1) then + if ((game.tick % interval_ticks) == interval_ticks - 1) then if (#global.rg.removal_list > 100) then OarcRegrowthRemoveAllChunks() SendBroadcastMsg("Map cleanup done, sorry for your loss.") @@ -300,14 +566,13 @@ end -- This function removes any chunks flagged but on demand. -- Controlled by the global.rg.force_removal_flag --- This function may be used outside of the normal regrowth modse. function RegrowthForceRemovalOnTick() -- Catch force remove flag - if (game.tick == global.rg.force_removal_flag+60) then + if (game.tick == global.rg.force_removal_flag + 60) then SendBroadcastMsg("Map cleanup (forced) in 30 seconds... Unused and old map chunks will be deleted!") end - if (game.tick == global.rg.force_removal_flag+(60*30 + 60)) then + if (game.tick == global.rg.force_removal_flag + (60 * 30 + 60)) then OarcRegrowthRemoveAllChunks() SendBroadcastMsg("Map cleanup done, sorry for your loss.") end @@ -315,71 +580,143 @@ end function WorldEaterSingleStep() - -- Make sure we have a valid iterator! - if (not global.rg.world_eater_iter or not global.rg.world_eater_iter.valid) then - global.rg.world_eater_iter = game.surfaces[GAME_SURFACE_NAME].get_chunks() - end - - local next_chunk = global.rg.world_eater_iter() - - -- Check if we reached the end - if (not next_chunk) then - global.rg.world_eater_iter = game.surfaces[GAME_SURFACE_NAME].get_chunks() - next_chunk = global.rg.world_eater_iter() - end + local next_chunk = GetNextChunkAndUpdateWorldEaterIter() + if (not next_chunk) then return end + local current_surface = global.rg.we_current_surface -- Do we have it in our map? - if (not global.rg.map[next_chunk.x] or not global.rg.map[next_chunk.x][next_chunk.y]) then + if (not global.rg[current_surface].map[next_chunk.x] or not global.rg[current_surface].map[next_chunk.x][next_chunk.y]) then return -- Chunk isn't in our map so we don't care? - end + end -- Search for any abandoned radars and destroy them? - local entities = game.surfaces[GAME_SURFACE_NAME].find_entities_filtered{area=next_chunk.area, - force={global.ocore.abandoned_force}, - name="radar"} - for k,v in pairs(entities) do + local abandoned_radars = game.surfaces[current_surface].find_entities_filtered { area = next_chunk.area, + force = { ABANDONED_FORCE_NAME }, + name = "radar" } + for k, v in pairs(abandoned_radars) do v.die(nil) end -- Search for any entities with _DESTROYED_ force and kill them. - entities = game.surfaces[GAME_SURFACE_NAME].find_entities_filtered{area=next_chunk.area, - force={global.ocore.destroyed_force}} - for k,v in pairs(entities) do - v.die(nil) + -- local destroy_entities = game.surfaces[current_surface].find_entities_filtered { area = next_chunk.area, + -- force = { DESTROYED_FORCE_NAME } } + -- for k, v in pairs(destroy_entities) do + -- v.die(nil) + -- end + + local c_timer = global.rg[current_surface].map[next_chunk.x][next_chunk.y] + + -- Only check chunnks that are flagged as "active". + -- Others are either permanent or will be handled by the default regrowth checks. + if (c_timer == REGROWTH_FLAG_ACTIVE) then + local area = { + left_top = { next_chunk.area.left_top.x - 8, next_chunk.area.left_top.y - 8 }, + right_bottom = { next_chunk.area.right_bottom.x + 8, next_chunk.area.right_bottom.y + 8 } + } + + local entities = game.surfaces[current_surface].find_entities_filtered { area = area, force = ENEMY_FORCES_NAMES_INCL_NEUTRAL, invert = true } + for _, v in pairs(entities) do + if (v.last_user) then + return -- This means we're done checking this chunk. It has an active player entity. + end + end + + local moving_entities = game.surfaces[current_surface].find_entities_filtered { + area = area, + type = { "character", "logistics-robot", "construction-robot", "car", "spider-vehicle" }, + } + if (#moving_entities > 0) then + return -- It's possible there are some moving entities with no last user set. + end + + -- Destroy the entities that lack an owner! (player was removed) + for _, v in pairs(entities) do + if (v and v.valid) then + v.die(nil) + end + end + -- SendBroadcastMsg(next_chunk.x .. "," .. next_chunk.y .. " WorldEaterSingleStep") + global.rg[current_surface].map[next_chunk.x][next_chunk.y] = game.tick -- Set the timer on it. + end +end - -- If the chunk isn't marked permament, then check if we can remove it - local c_timer = global.rg.map[next_chunk.x][next_chunk.y] - if (c_timer == -1) then - local area = {left_top = {next_chunk.area.left_top.x-8, next_chunk.area.left_top.y-8}, - right_bottom = {next_chunk.area.right_bottom.x+8, next_chunk.area.right_bottom.y+8}} +---Checks if a chunk has any player entities in or near it. +---@param surface_name string - The surface name to act on +---@param chunk ChunkPositionAndArea - The chunk position to check +---@return boolean +function CheckIfChunkHasAnyPlayerEntities(surface_name, chunk) - local entities = game.surfaces[GAME_SURFACE_NAME].find_entities_filtered{area=area, force={"enemy", "neutral"}, invert=true} - local total_count = #entities - local has_last_user_set = false + -- Check around the chunk for anything overlapping to be safe! + local area = { + left_top = { chunk.area.left_top.x - 8, chunk.area.left_top.y - 8 }, + right_bottom = { chunk.area.right_bottom.x + 8, chunk.area.right_bottom.y + 8 } + } - if (total_count > 0) then - for k,v in pairs(entities) do - if (v.last_user or (v.type == "character") or string.contains(v.type, "robot")) then - has_last_user_set = true - return -- This means we're done checking this chunk. - end - end + local entities = game.surfaces[surface_name].find_entities_filtered { area = area, force = ENEMY_FORCES_NAMES_INCL_NEUTRAL, invert = true } + for _, v in pairs(entities) do + if (v.last_user) then + return true -- YES there is player stuff here. + end + end - -- If all entities found have no last user, then KILL all entities! - if (not has_last_user_set) then - for k,v in pairs(entities) do - if (v and v.valid) then - v.die(nil) - end - end - -- SendBroadcastMsg(next_chunk.x .. "," .. next_chunk.y .. " WorldEaterSingleStep - ENTITIES FOUND") - global.rg.map[next_chunk.x][next_chunk.y] = game.tick -- Set the timer on it. + local moving_entities = game.surfaces[surface_name].find_entities_filtered { + area = area, + type = { "character", "logistics-robot", "construction-robot", "car", "spider-vehicle" }, + } + if (#moving_entities > 0) then + return true -- Any of these entities are player controlled and count! + end + + return false +end + + +---When an entity is built, if it is a spidertron we add it to our list +---@param event EventData.on_built_entity +---@return nil +function RegrowthOnBuiltEntity(event) + if (event.created_entity and event.created_entity.valid and event.created_entity.type == "spider-vehicle") then + + table.insert(global.rg.spidertrons, event.created_entity) + log("Added spidertron to regrowth tracking") + + if global.rg.spidertron_chunk_radius == nil then + global.rg.spidertron_chunk_radius = game.entity_prototypes["spidertron"].chunk_exploration_radius + end + end +end + + +---On tick, we refresh a single spidertron's area. +---@return nil +function RefreshSpidertronArea() + if (#global.rg.spidertrons > 0) then + + local spidertron = global.rg.spidertrons[global.rg_spidertron_index] + + if (spidertron and spidertron.valid) then + + --Check if this surface is active. + local surface_name = spidertron.surface.name + if (global.rg[surface_name] ~= nil) and (global.rg[surface_name].active) then + RefreshArea(spidertron.surface.name, spidertron.position, global.rg.spidertron_chunk_radius, 0) end + + UpdateSpidertronIndex() -- Go to next valid spidertron on the next tick else - -- SendBroadcastMsg(next_chunk.x .. "," .. next_chunk.y .. " WorldEaterSingleStep - NO ENTITIES FOUND") - global.rg.map[next_chunk.x][next_chunk.y] = game.tick -- Set the timer on it. + table.remove(global.rg.spidertrons, global.rg_spidertron_index) + log("Removed spidertron from regrowth tracking") end end end + +---Updates the spidertron index to the next one in the list. +---@return nil +function UpdateSpidertronIndex() + global.rg_spidertron_index = global.rg_spidertron_index + 1 + if (global.rg_spidertron_index > #global.rg.spidertrons) then + global.rg_spidertron_index = 1 + end +end \ No newline at end of file diff --git a/lib/rgcommand.lua b/lib/rgcommand.lua deleted file mode 100644 index bf3ac39..0000000 --- a/lib/rgcommand.lua +++ /dev/null @@ -1,49 +0,0 @@ - -local function RemoveTileGhosts() - local surface = game.player.surface - for c in surface.get_chunks() do - for key, entity in pairs(surface.find_entities_filtered({area={{c.x * 32, c.y * 32}, {c.x * 32 + 32, c.y * 32 + 32}}, name= "tile-ghost"})) do - entity.destroy() - end - end -end - -local function RemoveBlueprintedModulesGhosts() - local surface = game.player.surface - for c in surface.get_chunks() do - for key, entity in pairs(surface.find_entities_filtered({area={{c.x * 32, c.y * 32}, {c.x * 32 + 32, c.y * 32 + 32}}, name= "item-request-proxy"})) do - entity.destroy() - end - end -end - -local function RemoveGhostEntities() - local surface = game.player.surface - for c in surface.get_chunks() do - for key, entity in pairs(surface.find_entities_filtered({area={{c.x * 32, c.y * 32}, {c.x * 32 + 32, c.y * 32 + 32}}, name= "entity-ghost"})) do - entity.destroy() - end - end -end - - -commands.add_command("rg", "remove ghosts", function(command) - local player = game.players[command.player_index]; - if player ~= nil and player.admin then - if (command.parameter ~= nil) then - if command.parameter == "all" then - RemoveTileGhosts() - RemoveBlueprintedModulesGhosts() - RemoveGhostEntities() - elseif command.parameter == "tiles" then - RemoveTileGhosts() - elseif command.parameter == "modules" then - RemoveBlueprintedModulesGhosts() - elseif command.parameter == "entities" then - RemoveGhostEntities() - else - player.print("remove all ghostes | tiles | modules | entities"); - end - end - end -end) diff --git a/lib/rocket_launch.lua b/lib/rocket_launch.lua deleted file mode 100644 index 2585b6b..0000000 --- a/lib/rocket_launch.lua +++ /dev/null @@ -1,78 +0,0 @@ --- rocket_launch.lua --- May 2019 - --- This is meant to extract out any rocket launch related logic to support my oarc scenario designs. - -require("lib/oarc_utils") -require("config") - --------------------------------------------------------------------------------- --- Rocket Launch Event Code --- Controls the "win condition" --------------------------------------------------------------------------------- -function RocketLaunchEvent(event) - local force = event.rocket.force - - -- Notify players on force if rocket was launched without sat. - if event.rocket.get_item_count("satellite") == 0 then - for index, player in pairs(force.players) do - player.print("You launched the rocket, but you didn't put a satellite inside.") - end - return - end - - -- First ever sat launch - if not global.ocore.satellite_sent then - global.ocore.satellite_sent = {} - SendBroadcastMsg("Team " .. force.name .. " was the first to launch a rocket!") - ServerWriteFile("rocket_events", "Team " .. force.name .. " was the first to launch a rocket!" .. "\n") - - for name,player in pairs(game.players) do - SetOarcGuiTabEnabled(player, OARC_ROCKETS_GUI_TAB_NAME, true) - end - end - - -- Track additional satellites launched by this force - if global.ocore.satellite_sent[force.name] then - global.ocore.satellite_sent[force.name] = global.ocore.satellite_sent[force.name] + 1 - SendBroadcastMsg("Team " .. force.name .. " launched another rocket. Total " .. global.ocore.satellite_sent[force.name]) - ServerWriteFile("rocket_events", "Team " .. force.name .. " launched another rocket. Total " .. global.ocore.satellite_sent[force.name] .. "\n") - - -- First sat launch for this force. - else - -- game.set_game_state{game_finished=true, player_won=true, can_continue=true} - global.ocore.satellite_sent[force.name] = 1 - SendBroadcastMsg("Team " .. force.name .. " launched their first rocket!") - ServerWriteFile("rocket_events", "Team " .. force.name .. " launched their first rocket!" .. "\n") - - -- Unlock research and recipes - if global.ocfg.lock_goodies_rocket_launch then - for _,v in ipairs(LOCKED_TECHNOLOGIES) do - EnableTech(force, v.t) - end - for _,v in ipairs(LOCKED_RECIPES) do - if (force.technologies[v.r].researched) then - AddRecipe(force, v.r) - end - end - end - end -end - -function CreateRocketGuiTab(tab_container, player) - -- local frame = tab_container.add{type="frame", name="rocket-panel", caption="Satellites Launched:", direction = "vertical"} - - AddLabel(tab_container, nil, "Satellites Launched:", my_label_header_style) - - if (global.ocore.satellite_sent == nil) then - AddLabel(tab_container, nil, "No launches yet.", my_label_style) - else - for force_name,sat_count in pairs(global.ocore.satellite_sent) do - AddLabel(tab_container, - "rc_"..force_name, - "Team " .. force_name .. ": " .. tostring(sat_count), - my_label_style) - end - end -end - diff --git a/lib/scaled_enemies.lua b/lib/scaled_enemies.lua new file mode 100644 index 0000000..311628a --- /dev/null +++ b/lib/scaled_enemies.lua @@ -0,0 +1,369 @@ +-- This handles scaling enemies in a few different ways to make sure that all players can have a reasonable experience +-- even if they join the game late or are playing at a slower pace. + +-- TODO: Plan for new enemies in space DLC. +-- TODO: Plan for new enemies in space DLC. +-- TODO: Plan for new enemies in space DLC. + + +ENEMY_FORCE_EASY = "enemy-easy" +ENEMY_FORCE_MEDIUM = "enemy-medium" +ENEMY_FORCES_NAMES = {"enemy", ENEMY_FORCE_EASY, ENEMY_FORCE_MEDIUM} +ENEMY_FORCES_NAMES_INCL_NEUTRAL = {"enemy", ENEMY_FORCE_EASY, ENEMY_FORCE_MEDIUM, "neutral"} + +ENEMY_BUILT_TYPES = { "biter-spawner", "spitter-spawner", "small-worm-turret", "medium-worm-turret", "big-worm-turret", "behemoth-worm-turret" } + +---Create a few extra enemy forces with fixed evolution factors for scaling down near player bases. +---@return nil +function CreateEnemyForces() + + -- Create the enemy forces if they don't exist + for _,force_name in pairs(ENEMY_FORCES_NAMES) do + if (game.forces[force_name] == nil) then + game.create_force(force_name) + end + + local enemy_force = game.forces[force_name] + enemy_force.ai_controllable = true + end + + ConfigureEnemyForceRelationships() +end + +---Configures the friend and cease fire relationships between all forces and enemy forces. +---@return nil +function ConfigureEnemyForceRelationships() + for _,force in pairs(game.forces) do + + -- If this is an enemy force + if (TableContains(ENEMY_FORCES_NAMES, force.name)) then + + -- Make sure it IS friends with all other enemy forces. + for _,enemy_force_name in pairs(ENEMY_FORCES_NAMES) do + if (force.name ~= enemy_force_name) then -- Exclude self + local enemy_force = game.forces[enemy_force_name] + + force.set_friend(enemy_force, true) + force.set_cease_fire(enemy_force, true) + + enemy_force.set_friend(force, true) + enemy_force.set_cease_fire(force, true) + end + end + + -- If this is a non-enemy force + else + -- Make sure this force is NOT friends with any enemy forces. + for _,enemy_force_name in pairs(ENEMY_FORCES_NAMES) do + local enemy_force = game.forces[enemy_force_name] + + force.set_friend(enemy_force, false) + force.set_cease_fire(enemy_force, false) + + enemy_force.set_friend(force, false) + enemy_force.set_cease_fire(force, false) + end + end + + end +end + +---Configures the friend and cease fire relationships between all enemy forces and a new player force. +---@param new_player_force LuaForce +---@return nil +function ConfigureEnemyForceRelationshipsForNewPlayerForce(new_player_force) + for _,enemy_force_name in pairs(ENEMY_FORCES_NAMES) do + local enemy_force = game.forces[enemy_force_name] + + new_player_force.set_friend(enemy_force, false) + new_player_force.set_cease_fire(enemy_force, false) + + enemy_force.set_friend(new_player_force, false) + enemy_force.set_cease_fire(new_player_force, false) + end +end + + +--- Keep the enemy evolution factor in check. +---@return nil +function RestrictEnemyEvolutionOnTick() + local base_evo_factor = game.forces["enemy"].evolution_factor + + -- Restrict the evolution factor of the enemy forces + game.forces[ENEMY_FORCE_EASY].evolution_factor = math.min(base_evo_factor, global.ocfg.gameplay.modified_enemy_easy_evo) + game.forces[ENEMY_FORCE_MEDIUM].evolution_factor = math.min(base_evo_factor, global.ocfg.gameplay.modified_enemy_medium_evo) +end + +---Downgrades worms based on distance from origin and near/far spawn distances. +---This helps make sure worms aren't too overwhelming even at these further spawn distances. +---@param event EventData.on_chunk_generated +---@return nil +function DowngradeWormsDistanceBasedOnChunkGenerate(event) + + ---@type OarcConfigGameplaySettings + local gameplay = global.ocfg.gameplay + + if (util.distance({ x = 0, y = 0 }, event.area.left_top) < (gameplay.near_spawn_distance * CHUNK_SIZE)) then + DowngradeWormsInArea(event.surface, event.area, 100, 100, 100) + elseif (util.distance({ x = 0, y = 0 }, event.area.left_top) < (gameplay.far_spawn_distance * CHUNK_SIZE)) then + DowngradeWormsInArea(event.surface, event.area, 50, 90, 100) + elseif (util.distance({ x = 0, y = 0 }, event.area.left_top) < (gameplay.far_spawn_distance * CHUNK_SIZE * 2)) then + DowngradeWormsInArea(event.surface, event.area, 20, 80, 97) + else + DowngradeWormsInArea(event.surface, event.area, 0, 20, 90) + end +end + +---Downgrades enemies based on distance from origin and near/far spawn distances. +---@param event EventData.on_chunk_generated +---@return nil +function DowngradeAndReduceEnemiesOnChunkGenerate(event) + + local surface = event.surface + local chunk_area = event.area + + local closest_spawn = GetClosestUniqueSpawn(surface.name, chunk_area.left_top) + if (closest_spawn == nil) then return end + + local spawn_config --[[@as OarcConfigSpawn]] = global.ocfg.surfaces_config[surface.name].spawn_config + local chunkAreaCenter = { + x = chunk_area.left_top.x + (CHUNK_SIZE / 2), + y = chunk_area.left_top.y + (CHUNK_SIZE / 2) + } + + -- Make chunks near a spawn safe by removing enemies + if (util.distance(closest_spawn.position, chunkAreaCenter) < spawn_config.safe_area.safe_radius) then + RemoveEnemiesInArea(surface, chunk_area) + + -- Create a warning area with heavily reduced enemies + elseif (util.distance(closest_spawn.position, chunkAreaCenter) < spawn_config.safe_area.warn_radius) then + + -- TODO: Refactor this to reduce calls to find_entities_filtered! + ReduceEnemiesInArea(surface, chunk_area, spawn_config.safe_area.warn_reduction) + RemoveWormsInArea(surface, chunk_area, false, true, true, true) -- remove all non-small worms. + ConvertEnemiesToOtherForceInArea(surface, chunk_area, ENEMY_FORCE_EASY) + + -- Create a third area with moderately reduced enemies + elseif (util.distance(closest_spawn.position, chunkAreaCenter) < spawn_config.safe_area.danger_radius) then + + -- TODO: Refactor this to reduce calls to find_entities_filtered! + ReduceEnemiesInArea(surface, chunk_area, spawn_config.safe_area.danger_reduction) + RemoveWormsInArea(surface, chunk_area, false, false, true, true) -- remove all huge/behemoth worms. + ConvertEnemiesToOtherForceInArea(surface, chunk_area, ENEMY_FORCE_MEDIUM) + end + +end + +---Convenient way to remove aliens, just provide an area +---@param surface LuaSurface +---@param area BoundingBox +---@return nil +function RemoveEnemiesInArea(surface, area) + for _, entity in pairs(surface.find_entities_filtered { area = area, force = "enemy" }) do + entity.destroy() + end +end + +---Make an area safer, randomly removes enemies based on a reduction factor. +---@param surface LuaSurface +---@param area BoundingBox +---@param reductionFactor integer Reduction factor divides the enemy spawns by that number. 2 = half, 3 = third, etc... +---@return nil +function ReduceEnemiesInArea(surface, area, reductionFactor) + for _, entity in pairs(surface.find_entities_filtered { area = area, force = "enemy" }) do + if (math.random(0, reductionFactor) > 0) then + entity.destroy() + end + end +end + +---Downgrades worms in an area based on chance. 100% small would mean all worms are changed to small. +---@param surface LuaSurface +---@param area BoundingBox +---@param small_percent integer ---Chance to change to small worm +---@param medium_percent integer +---@param big_percent integer +---@return nil +function DowngradeWormsInArea(surface, area, small_percent, medium_percent, big_percent) + -- Leave out "small-worm-turret" as it's the lowest. + local worm_types = { "medium-worm-turret", "big-worm-turret", "behemoth-worm-turret" } + + for _, entity in pairs(surface.find_entities_filtered { area = area, name = worm_types }) do + -- Roll a number between 0-100 + local rand_percent = math.random(0, 100) + local worm_pos = entity.position + local worm_name = entity.name + + -- If number is less than small percent, change to small + if (rand_percent <= small_percent) then + if (not (worm_name == "small-worm-turret")) then + entity.destroy() + surface.create_entity { name = "small-worm-turret", position = worm_pos, force = game.forces.enemy } + end + + -- ELSE If number is less than medium percent, change to small + elseif (rand_percent <= medium_percent) then + if (not (worm_name == "medium-worm-turret")) then + entity.destroy() + surface.create_entity { name = "medium-worm-turret", position = worm_pos, force = game.forces.enemy } + end + + -- ELSE If number is less than big percent, change to small + elseif (rand_percent <= big_percent) then + if (not (worm_name == "big-worm-turret")) then + entity.destroy() + surface.create_entity { name = "big-worm-turret", position = worm_pos, force = game.forces.enemy } + end + + -- ELSE ignore it. + end + end +end + +---A function to help me remove worms in an area. Yeah kind of an unecessary wrapper, but makes my life easier to remember the worm types. +---@param surface LuaSurface +---@param area BoundingBox +---@param small boolean +---@param medium boolean +---@param big boolean +---@param behemoth boolean +---@return nil +function RemoveWormsInArea(surface, area, small, medium, big, behemoth) + local worm_types = {} + + if (small) then + table.insert(worm_types, "small-worm-turret") + end + if (medium) then + table.insert(worm_types, "medium-worm-turret") + end + if (big) then + table.insert(worm_types, "big-worm-turret") + end + if (behemoth) then + table.insert(worm_types, "behemoth-worm-turret") + end + + -- Destroy + if (#worm_types > 0) then + for _, entity in pairs(surface.find_entities_filtered { area = area, name = worm_types }) do + entity.destroy() + end + else + log("RemoveWormsInArea had empty worm_types list!") + end +end + +---Converts all enemies in the base enemy force in an area to easy force. +---@param surface LuaSurface +---@param area BoundingBox +---@param force_name string +---@return nil +function ConvertEnemiesToOtherForceInArea(surface, area, force_name) + for _, entity in pairs(surface.find_entities_filtered { area = area, force = "enemy" }) do + entity.force = game.forces[force_name] + end +end + +---Converts new enemy bases to different enemy forces if they are near to player bases. +---@param event EventData.on_biter_base_built +---@return nil +function ChangeEnemySpawnersToOtherForceOnBuilt(event) + if (not event.entity or not event.entity.position or not TableContains(ENEMY_FORCES_NAMES, event.entity.force.name)) then + log("ChangeEnemySpawnersToOtherForceOnBuilt Unexpected entity or force?") + return + end + + local enemy_pos = event.entity.position + local surface = event.entity.surface + local enemy_name = event.entity.name + + if (not TableContains(ENEMY_BUILT_TYPES, enemy_name)) then + log("ChangeEnemySpawnersToOtherForceOnBuilt Unexpected entity name? " .. enemy_name) + return + end + + local closest_spawn = GetClosestUniqueSpawn(surface.name, enemy_pos) + if (closest_spawn == nil) then return end + + -- No enemies inside safe radius! + if (util.distance(enemy_pos, closest_spawn.position) < global.ocfg.surfaces_config[surface.name].spawn_config.safe_area.safe_radius) then + event.entity.destroy() + + -- Warn distance should be EASY + elseif (util.distance(enemy_pos, closest_spawn.position) < global.ocfg.surfaces_config[surface.name].spawn_config.safe_area.warn_radius) then + event.entity.force = game.forces[ENEMY_FORCE_EASY] + + -- Danger distance should be MEDIUM + elseif (util.distance(enemy_pos, closest_spawn.position) < global.ocfg.surfaces_config[surface.name].spawn_config.safe_area.danger_radius) then + event.entity.force = game.forces[ENEMY_FORCE_MEDIUM] + + -- Otherwise make sure they are on the base enemy force (stops easy enemies from spreading out too far). + else + event.entity.force = game.forces["enemy"] + end + +end + +-- I wrote this to ensure everyone gets safer spawns regardless of evolution level. +-- This is intended to downgrade any biters/spitters spawning near player bases. +-- I'm not sure the performance impact of this but I'm hoping it's not bad. +---@param event EventData.on_entity_spawned|EventData.on_biter_base_built +---@return nil +function ModifyEnemySpawnsNearPlayerStartingAreas(event) + if (not event.entity or not (event.entity.force.name == "enemy") or not event.entity.position) then + log("ModifyBiterSpawns - Unexpected use.") + return + end + + local enemy_pos = event.entity.position + local surface = event.entity.surface + local enemy_name = event.entity.name + + local closest_spawn = GetClosestUniqueSpawn(surface.name, enemy_pos) + + if (closest_spawn == nil) then + -- log("GetClosestUniqueSpawn ERROR - None found?") + return + end + + -- No enemies inside safe radius! + if (util.distance(enemy_pos, closest_spawn.position) < global.ocfg.surfaces_config[surface.name].spawn_config.safe_area.safe_radius) then + event.entity.destroy() + + -- Warn distance is all SMALL only. + elseif (util.distance(enemy_pos, closest_spawn.position) < global.ocfg.surfaces_config[surface.name].spawn_config.safe_area.warn_radius) then + if ((enemy_name == "biter-spawner") or (enemy_name == "spitter-spawner")) then + event.entity.force = game.forces["enemy-easy"] + + elseif ((enemy_name == "big-biter") or (enemy_name == "behemoth-biter") or (enemy_name == "medium-biter")) then + event.entity.destroy() + surface.create_entity { name = "small-biter", position = enemy_pos, force = game.forces["enemy-easy"] } + -- log("Downgraded biter close to spawn.") + elseif ((enemy_name == "big-spitter") or (enemy_name == "behemoth-spitter") or (enemy_name == "medium-spitter")) then + event.entity.destroy() + surface.create_entity { name = "small-spitter", position = enemy_pos, force = game.forces["enemy-easy"] } + -- log("Downgraded spitter close to spawn.") + elseif ((enemy_name == "big-worm-turret") or (enemy_name == "behemoth-worm-turret") or (enemy_name == "medium-worm-turret")) then + event.entity.destroy() + surface.create_entity { name = "small-worm-turret", position = enemy_pos, force = game.forces["enemy-easy"] } + -- log("Downgraded worm close to spawn.") + end + + -- Danger distance is MEDIUM max. + elseif (util.distance(enemy_pos, closest_spawn.position) < global.ocfg.surfaces_config[surface.name].spawn_config.safe_area.danger_radius) then + if ((enemy_name == "big-biter") or (enemy_name == "behemoth-biter")) then + event.entity.destroy() + surface.create_entity { name = "medium-biter", position = enemy_pos, force = game.forces.enemy } + -- log("Downgraded biter further from spawn.") + elseif ((enemy_name == "big-spitter") or (enemy_name == "behemoth-spitter")) then + event.entity.destroy() + surface.create_entity { name = "medium-spitter", position = enemy_pos, force = game.forces.enemy } + -- log("Downgraded spitter further from spawn + elseif ((enemy_name == "big-worm-turret") or (enemy_name == "behemoth-worm-turret")) then + event.entity.destroy() + surface.create_entity { name = "medium-worm-turret", position = enemy_pos, force = game.forces.enemy } + -- log("Downgraded worm further from spawn.") + end + end +end \ No newline at end of file diff --git a/lib/separate_spawns.lua b/lib/separate_spawns.lua index 1e0ddb2..cc3669e 100644 --- a/lib/separate_spawns.lua +++ b/lib/separate_spawns.lua @@ -1,467 +1,637 @@ --- separate_spawns.lua --- Nov 2016 --- -- Code that handles everything regarding giving each player a separate spawn --- Includes the GUI stuff -require("lib/oarc_utils") -require("config") +local util = require("util") local crash_site = require("crash-site") --[[ - ___ _ _ ___ _____ + ___ _ _ ___ _____ |_ _|| \| ||_ _||_ _| - | | | .` | | | | | - |___||_|\_||___| |_| - + | | | .` | | | | | + |___||_|\_||___| |_| + --]] --- Initializes the globals used to track the special spawn and player --- status information +-- 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() - -- Core global to help me organize shit. - if (global.ocore == nil) then - global.ocore = {} + -- Contains a table of entries for each surface. This tracks which surfaces allow spawning? + --[[@type table]] + global.oarc_surfaces = {} + for _, surface in pairs(game.surfaces) do + SeparateSpawnsInitSurface(surface.name) end - -- This contains each player's spawn point. Literally where they will respawn. - -- There is a way in game to change this under one of the little menu features I added. - if (global.ocore.playerSpawns == nil) then - global.ocore.playerSpawns = {} - end + -- 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]] + global.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. - -- Each entry looks like this: {pos={x,y},moat=bool,vanilla=bool} - if (global.ocore.uniqueSpawns == nil) then - global.ocore.uniqueSpawns = {} - end - - -- List of available vanilla spawns - if (global.vanillaSpawns == nil) then - global.vanillaSpawns = {} - end - - -- This keeps a list of any player that has shared their base. - -- Each entry contains information about if it's open, spawn pos, and players in the group. - if (global.ocore.sharedSpawns == nil) then - global.ocore.sharedSpawns = {} - end + -- This is what chunk generation checks against, and is used for shared spawn tracking, and more. + ---@type OarcUniqueSpawnsTable + global.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. - if (global.ocore.playerCooldowns == nil) then - global.ocore.playerCooldowns = {} - end - - -- List of players in the "waiting room" for a buddy spawn. - -- They show up in the list to select when doing a buddy spawn. - if (global.ocore.waitingBuddies == nil) then - global.ocore.waitingBuddies = {} - end + --[[@type OarcPlayerCooldownsTable]] + global.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. - if (global.ocore.delayedSpawns == nil) then - global.ocore.delayedSpawns = {} - end + --[[@type OarcDelayedSpawnsTable]] + global.delayed_spawns = {} - -- This is what I use to communicate a buddy spawn request between the buddies. - -- This contains information of who is asking, and what options were selected. - if (global.ocore.buddySpawnOpts == nil) then - global.ocore.buddySpawnOpts = {} - end + -- 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]] + global.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.
+ -- global.buddy_pairs[player.name] = requesterName
+ -- global.buddy_pairs[requesterName] = player.name
+ --[[@type table]] + global.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]] + global.oarc_renders_fadeout = {} + + -- Special forces for when players with their own force want a reset. + game.create_force(ABANDONED_FORCE_NAME) + -- game.create_force(DESTROYED_FORCE_NAME) + + -- Special enemy forces for scaling down enemies near player bases. + CreateEnemyForces() + + -- 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(global.ocfg.gameplay.main_force_name) + main_force.set_spawn_position({ x = 0, y = 0 }, global.ocfg.gameplay.default_surface) + + CreateHoldingPenPermissionsGroup() +end - -- Silo info - if (global.siloPosition == nil) then - global.siloPosition = {} +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 - -- 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. - if (global.ocore.buddyPairs == nil) then - global.ocore.buddyPairs = {} + 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 - -- Rendering fancy fadeouts. - if (global.oarc_renders_fadeout == nil) then - global.oarc_renders_fadeout = {} + -- 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 (global.oarc_surfaces == nil) then + error("global.oarc_surfaces not initialized yet?! " .. surface_name) end - -- 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 = CreateForce(global.ocfg.main_force) - main_force.set_spawn_position({x=0,y=0}, GAME_SURFACE_NAME) + if IsSurfaceBlacklisted(surface_name) then return end - -- Special forces for when players with their own force want a reset. - global.ocore.abandoned_force = "_ABANDONED_" - global.ocore.destroyed_force = "_DESTROYED_" - game.create_force(global.ocore.abandoned_force) - game.create_force(global.ocore.destroyed_force) + -- Add the surface to the list of surfaces that allow spawns with value from config. + if global.ocfg.gameplay.default_allow_spawning_on_other_surfaces then + global.oarc_surfaces[surface_name] = true + + -- Otherwise only allow the default surface (by default) + else + global.oarc_surfaces[surface_name] = (surface_name == global.ocfg.gameplay.default_surface) + end + + -- Make sure it has a surface configuration entry + if (global.oarc_surfaces[surface_name] and global.ocfg.surfaces_config[surface_name] == nil) then + log("Surface does NOT have a config entry, defaulting to nauvis entry for new surface: " .. surface_name) + global.ocfg.surfaces_config[surface_name] = global.ocfg.surfaces_config["nauvis"] + 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 global.oarc_surfaces[surface_name] ~= nil then + log("WARNING!! - Surface deleted event not validated/implemented yet! " .. surface_name) + global.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 / | _| | / \__ \| _/| _|| (__ | | | _| | || (__ + | _/| |__ / _ \\ V / | _| | / \__ \| _/| _|| (__ | | | _| | || (__ |_| |____|/_/ \_\|_| |___||_|_\ |___/|_| |___|\___||___||_| |___|\___| - + --]] --- When a new player is created, present the spawn options --- Assign them to the main force so they can communicate with the team --- without shouting. -function SeparateSpawnsPlayerCreated(player_index, clear_inv) +-- 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] - -- Make sure spawn control tab is disabled - SetOarcGuiTabEnabled(player, OARC_SPAWN_CTRL_GUI_NAME, false) - SwitchOarcGuiTab(player, OARC_GAME_OPTS_GUI_TAB_NAME) + SafeTeleport(player, game.surfaces[HOLDING_PEN_SURFACE_NAME], { x = 0, y = 0 }) - -- If they are a new player, put them on the main force. - if (player.force.name == "player") then - player.force = global.ocfg.main_force - end + player.force = global.ocfg.gameplay.main_force_name -- Put them on the main force. - -- Reset counts for map feature usage for this player. - OarcMapFeaturePlayerCreatedEvent(player) + if (not player.admin) then + player.permission_group = game.permissions.get_group("holding_pen") + end - -- Ensure cleared inventory! - if (clear_inv) then - player.get_inventory(defines.inventory.character_main ).clear() - player.get_inventory(defines.inventory.character_guns).clear() - player.get_inventory(defines.inventory.character_ammo).clear() - player.get_inventory(defines.inventory.character_armor).clear() - player.get_inventory(defines.inventory.character_trash).clear() + if (global.player_respawns[player.name] == nil) then + global.player_respawns[player.name] = {} + end + if (global.player_cooldowns[player.name] == nil) then + global.player_cooldowns[player.name] = { setRespawn = game.tick } end - HideOarcGui(player) - HideOarcStore(player) + -- 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] - SendPlayerToSpawn(player) + local surface_name = player.surface.name + + -- It's possible if player is dead, and then resets, we don't want to do anything else. + if (player.surface.name == HOLDING_PEN_SURFACE_NAME) then return end + + -- If the mod isn't active on this surface, then ignore it. + if (not global.oarc_surfaces[surface_name]) then return end + + SendPlayerToSpawn(surface_name, player) + 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 < (global.ocfg.gameplay.minimum_online_time * TICKS_PER_MINUTE))) then + SendBroadcastMsg({ "oarc-player-left-early", player.name, global.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 event EventData.on_player_changed_surface +---@return nil +function SeparateSpawnsPlayerChangedSurface(event) + if (not global.ocfg.gameplay.enable_secondary_spawns) then return end + + local player = game.players[event.player_index] + + -- Check if player has been init'd yet. If not, then ignore it. + if (global.player_respawns[player.name] == nil) then return end + + -- If the mod isn't active on this surface, then ignore it. + if (not global.oarc_surfaces[player.surface.name]) then return end + + -- If this is their first time on the planet, create a secondary spawn point for them. + -- TODO: Check for buddy and shared spawn hosts? + if (global.unique_spawns[player.surface.name] == nil) or (global.unique_spawns[player.surface.name][player.name] == nil) then + log("WARNING - THIS IS NOT FULLY IMPLEMENTED YET!!") + SecondarySpawn(player, player.surface) + end +end --[[ - ___ ___ _ __ __ _ _ ___ ___ _____ _ _ ___ + ___ ___ _ __ __ _ _ ___ ___ _____ _ _ ___ / __|| _ \ /_\\ \ / /| \| | / __|| __||_ _|| | | || _ \ \__ \| _// _ \\ \/\/ / | .` | \__ \| _| | | | |_| || _/ - |___/|_| /_/ \_\\_/\_/ |_|\_| |___/|___| |_| \___/ |_| - ---]] + |___/|_| /_/ \_\\_/\_/ |_|\_| |___/|___| |_| \___/ |_| --- Add a spawn to the shared spawn global --- Used for tracking which players are assigned to it, where it is and if --- it is open for new players to join -function CreateNewSharedSpawn(player) - global.ocore.sharedSpawns[player.name] = {openAccess=true, - position=global.ocore.playerSpawns[player.name], - players={}} -end +--]] -- Generate the basic starter resource around a given location. -function GenerateStartingResources(surface, pos) +---@param surface LuaSurface +---@param position TilePosition --The center of the spawn area +---@return nil +function GenerateStartingResources(surface, position) - local rand_settings = global.ocfg.spawn_config.resource_rand_pos_settings + local size_mod = global.ocfg.resource_placement.size_multiplier + local amount_mod = global.ocfg.resource_placement.amount_multiplier -- Generate all resource tile patches - if (not rand_settings.enabled) then - for t_name,t_data in pairs (global.ocfg.spawn_config.resource_tiles) do - local pos = {x=pos.x+t_data.x_offset, y=pos.y+t_data.y_offset} - GenerateResourcePatch(surface, t_name, t_data.size, pos, t_data.amount) + if (not global.ocfg.resource_placement.enabled) then + for r_name, r_data in pairs(global.ocfg.surfaces_config[surface.name].spawn_config.solid_resources --[[@as table]]) 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 + + -- Generate resources in random order around the spawn point. Tweak in config.lua else + + if (global.ocfg.spawn_general.shape == SPAWN_SHAPE_CHOICE_CIRCLE) or (global.ocfg.spawn_general.shape == SPAWN_SHAPE_CHOICE_OCTAGON) then + PlaceResourcesInSemiCircle(surface, position, size_mod, amount_mod) + elseif (global.ocfg.spawn_general.shape == SPAWN_SHAPE_CHOICE_SQUARE) then + PlaceResourcesInSquare(surface, position, size_mod, amount_mod) + end + end - -- Create list of resource tiles - local r_list = {} - for k,_ in pairs(global.ocfg.spawn_config.resource_tiles) do - if (k ~= "") then - table.insert(r_list, k) - end + -- Generate special fluid resource patches (oil) + -- Reference position is the bottom of the spawn area. + local fluid_ref_pos = { x = position.x, + y = position.y + global.ocfg.spawn_general.spawn_radius_tiles } + for r_name, r_data in pairs(global.ocfg.surfaces_config[surface.name].spawn_config.fluid_resources --[[@as table]]) 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 - local shuffled_list = FYShuffle(r_list) + end +end - -- This places resources in a semi-circle - -- Tweak in config.lua - local angle_offset = rand_settings.angle_offset - local num_resources = TableLength(global.ocfg.spawn_config.resource_tiles) - local theta = ((rand_settings.angle_final - rand_settings.angle_offset) / num_resources); - local count = 0 +---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 + local r_list = {} + for r_name, _ in pairs(global.ocfg.surfaces_config[surface.name].spawn_config.solid_resources --[[@as table]]) do + if (r_name ~= "") then + table.insert(r_list, r_name) + end + end + ---@type table + local shuffled_list = FYShuffle(r_list) - for _,k_name in pairs (shuffled_list) do - local angle = (theta * count) + angle_offset; + -- This places resources in a semi-circle + local angle_offset = global.ocfg.resource_placement.angle_offset + local num_resources = table_size(global.ocfg.surfaces_config[surface.name].spawn_config.solid_resources) + local theta = ((global.ocfg.resource_placement.angle_final - global.ocfg.resource_placement.angle_offset) / num_resources); + local count = 0 - local tx = (rand_settings.radius * math.cos(angle)) + pos.x - local ty = (rand_settings.radius * math.sin(angle)) + pos.y + local radius = global.ocfg.spawn_general.spawn_radius_tiles - global.ocfg.resource_placement.distance_to_edge - local pos = {x=math.floor(tx), y=math.floor(ty)} - GenerateResourcePatch(surface, k_name, global.ocfg.spawn_config.resource_tiles[k_name].size, pos, global.ocfg.spawn_config.resource_tiles[k_name].amount) - count = count+1 - end + for _, r_name in pairs(shuffled_list) do + local angle = (theta * count) + angle_offset; + + 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 = global.ocfg.surfaces_config[surface.name].spawn_config.solid_resources[r_name] + GenerateResourcePatch(surface, r_name, resourceConfig.size * size_mod, pos, resourceConfig.amount * amount_mod) + count = count + 1 end +end - -- Generate special resource patches (oil) - for p_name,p_data in pairs (global.ocfg.spawn_config.resource_patches) do - local oil_patch_x=pos.x+p_data.x_offset_start - local oil_patch_y=pos.y+p_data.y_offset_start - for i=1,p_data.num_patches do - surface.create_entity({name=p_name, amount=p_data.amount, - position={oil_patch_x, oil_patch_y}}) - oil_patch_x=oil_patch_x+p_data.x_offset_next - oil_patch_y=oil_patch_y+p_data.y_offset_next +---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 + local r_list = {} + for r_name, _ in pairs(global.ocfg.surfaces_config[surface.name].spawn_config.solid_resources --[[@as table]]) do + if (r_name ~= "") then + table.insert(r_list, r_name) end end -end + ---@type table + local shuffled_list = FYShuffle(r_list) -function SendPlayerToNewSpawnAndCreateIt(delayedSpawn) + -- Get the top left position of the spawn area + local resource_position = { x = position.x - global.ocfg.spawn_general.spawn_radius_tiles, + y = position.y - global.ocfg.spawn_general.spawn_radius_tiles } - -- DOUBLE CHECK and make sure the area is super safe. - ClearNearbyEnemies(delayedSpawn.pos, global.ocfg.spawn_config.safe_area.safe_radius, game.surfaces[GAME_SURFACE_NAME]) - - if (not delayedSpawn.vanilla) then - - -- Generate water strip only if we don't have a moat. - if (not delayedSpawn.moat) then - local water_data = global.ocfg.spawn_config.water - CreateWaterStrip(game.surfaces[GAME_SURFACE_NAME], - {x=delayedSpawn.pos.x+water_data.x_offset, y=delayedSpawn.pos.y+water_data.y_offset}, - water_data.length) - CreateWaterStrip(game.surfaces[GAME_SURFACE_NAME], - {x=delayedSpawn.pos.x+water_data.x_offset, y=delayedSpawn.pos.y+water_data.y_offset+1}, - water_data.length) - end + -- Offset the starting position + resource_position.x = resource_position.x + global.ocfg.resource_placement.horizontal_offset + resource_position.y = resource_position.y + global.ocfg.resource_placement.vertical_offset + + -- Place vertically using linear spacing + for _, r_name in pairs(shuffled_list) do + local resourceConfig = global.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 + global.ocfg.resource_placement.linear_spacing + end +end - -- Create the spawn resources here - GenerateStartingResources(game.surfaces[GAME_SURFACE_NAME], delayedSpawn.pos) +---Sends the player to their spawn point +---@param delayed_spawn OarcDelayedSpawn +---@return nil +function SendPlayerToNewSpawnAndCreateIt(delayed_spawn) + local ocfg --[[@as OarcConfig]] = global.ocfg + local spawn_config = ocfg.surfaces_config[delayed_spawn.surface].spawn_config + -- DOUBLE CHECK and make sure the area is super safe. + ClearNearbyEnemies(delayed_spawn.position, spawn_config.safe_area.safe_radius, + game.surfaces[delayed_spawn.surface]) + + -- Generate water strip only if we don't have a moat. + if (not delayed_spawn.moat) then + 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 - global.ocfg.spawn_general.spawn_radius_tiles + } + CreateWaterStrip(game.surfaces[delayed_spawn.surface], + { x = reference_pos.x + water_data.x_offset, y = reference_pos.y + water_data.y_offset }, + water_data.length) + CreateWaterStrip(game.surfaces[delayed_spawn.surface], + { x = reference_pos.x + water_data.x_offset, y = reference_pos.y + water_data.y_offset + 1 }, + water_data.length) + end + + -- Create the spawn resources here + GenerateStartingResources(game.surfaces[delayed_spawn.surface], delayed_spawn.position) + + -- Reference position is RIGHT (WEST) of the spawn area. + local sharing_ref_pos = { + x = delayed_spawn.position.x + global.ocfg.spawn_general.spawn_radius_tiles, + y = delayed_spawn.position.y + } + + -- Create shared power poles + if (ocfg.gameplay.enable_shared_power) 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(game.surfaces[delayed_spawn.surface], power_pole_position) + end + + -- Create shared chest + if (ocfg.gameplay.enable_shared_chest) 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(game.surfaces[delayed_spawn.surface], chest_position) end -- Send the player to that position - local player = game.players[delayedSpawn.playerName] - SafeTeleport(player, game.surfaces[GAME_SURFACE_NAME], delayedSpawn.pos) + local player = game.players[delayed_spawn.playerName] + SendPlayerToSpawn(delayed_spawn.surface, player) GivePlayerStarterItems(player) -- Render some welcoming text... - DisplayWelcomeGroundTextAtSpawn(player, delayedSpawn.pos) + DisplayWelcomeGroundTextAtSpawn(player, delayed_spawn.surface, delayed_spawn.position) -- Chart the area. - ChartArea(player.force, delayedSpawn.pos, math.ceil(global.ocfg.spawn_config.gen_settings.land_area_tiles/CHUNK_SIZE), player.surface) + ChartArea(player.force, delayed_spawn.position, math.ceil(global.ocfg.spawn_general.spawn_radius_tiles / CHUNK_SIZE), + player.surface) if (player.gui.screen.wait_for_spawn_dialog ~= nil) then player.gui.screen.wait_for_spawn_dialog.destroy() end - if (global.ocfg.enable_chest_sharing and not delayedSpawn.vanilla) then - - local x_dist = global.ocfg.spawn_config.resource_rand_pos_settings.radius - - -- Shared electricity IO pair of scripted electric-energy-interfaces - SharedEnergySpawnInput(player, {x=delayedSpawn.pos.x+x_dist, y=delayedSpawn.pos.y-11}) - SharedEnergySpawnOutput(player, {x=delayedSpawn.pos.x+x_dist, y=delayedSpawn.pos.y+10}) - - -- Input Chests - SharedChestsSpawnInput(player, {x=delayedSpawn.pos.x+x_dist, y=delayedSpawn.pos.y-7}) - SharedChestsSpawnInput(player, {x=delayedSpawn.pos.x+x_dist, y=delayedSpawn.pos.y-6}) - - -- Tile arrows to help indicate - CreateTileArrow(game.surfaces[GAME_SURFACE_NAME], {x=delayedSpawn.pos.x+x_dist-4, y=delayedSpawn.pos.y-7}, "RIGHT") - CreateTileArrow(game.surfaces[GAME_SURFACE_NAME], {x=delayedSpawn.pos.x+x_dist+1, y=delayedSpawn.pos.y-7}, "LEFT") - - -- Combinators for monitoring items in the network. - SharedChestsSpawnCombinators(player, - {x=delayedSpawn.pos.x+x_dist-1, y=delayedSpawn.pos.y-2}, -- Ctrl - {x=delayedSpawn.pos.x+x_dist-1, y=delayedSpawn.pos.y}) -- Status - - - SharedChestsSpawnOutput(player, {x=delayedSpawn.pos.x+x_dist, y=delayedSpawn.pos.y+4}, true) - SharedChestsSpawnOutput(player, {x=delayedSpawn.pos.x+x_dist, y=delayedSpawn.pos.y+5}, true) - - -- Tile arrows to help indicate - CreateTileArrow(game.surfaces[GAME_SURFACE_NAME], {x=delayedSpawn.pos.x+x_dist-4, y=delayedSpawn.pos.y+4}, "LEFT") - CreateTileArrow(game.surfaces[GAME_SURFACE_NAME], {x=delayedSpawn.pos.x+x_dist+1, y=delayedSpawn.pos.y+4}, "RIGHT") - - -- Cutscene to force the player to witness my brilliance - -- player.set_controller{type=defines.controllers.cutscene,waypoints={{position={x=delayedSpawn.pos.x+x_dist, - -- y=delayedSpawn.pos.y},transition_time=150,time_to_wait=150,zoom=0.8},{target=player.character,transition_time=60,time_to_wait=30,zoom=0.8}}, - -- final_transition_time=45} - end - - if (global.ocfg.spawn_config.gen_settings.crashed_ship) then - crash_site.create_crash_site(game.surfaces[GAME_SURFACE_NAME], - {x=delayedSpawn.pos.x+15, y=delayedSpawn.pos.y-25}, - global.ocfg.spawn_config.gen_settings.crashed_ship_resources, - global.ocfg.spawn_config.gen_settings.crashed_ship_wreakage) + if (ocfg.surfaces_config[delayed_spawn.surface].starting_items.crashed_ship) then + crash_site.create_crash_site(game.surfaces[delayed_spawn.surface], + { x = delayed_spawn.position.x + 15, y = delayed_spawn.position.y - 25 }, + ocfg.surfaces_config[delayed_spawn.surface].starting_items.crashed_ship_resources, + ocfg.surfaces_config[delayed_spawn.surface].starting_items.crashed_ship_wreakage) end - end -function DisplayWelcomeGroundTextAtSpawn(player, pos) - +---Displays some welcoming text at the spawn point on the ground. Fades out over time. +---@param player LuaPlayer +---@param surface LuaSurface|string +---@param position MapPosition +---@return nil +function DisplayWelcomeGroundTextAtSpawn(player, surface, position) -- Render some welcoming text... - local tcolor = {0.9, 0.7, 0.3, 0.8} + local tcolor = { 0.9, 0.7, 0.3, 0.8 } local ttl = 2000 - local rid1 = rendering.draw_text{text="Welcome", - surface=game.surfaces[GAME_SURFACE_NAME], - target={x=pos.x-21, y=pos.y-15}, - color=tcolor, - scale=20, - font="compi", - time_to_live=ttl, - -- players={player}, - draw_on_ground=true, - orientation=0, - -- alignment=center, - scale_with_zoom=false, - only_in_alt_mode=false} - local rid2 = rendering.draw_text{text="Home", - surface=game.surfaces[GAME_SURFACE_NAME], - target={x=pos.x-14, y=pos.y-5}, - color=tcolor, - scale=20, - font="compi", - time_to_live=ttl, - -- players={player}, - draw_on_ground=true, - orientation=0, - -- alignment=center, - scale_with_zoom=false, - only_in_alt_mode=false} + local rid1 = rendering.draw_text { text = "Welcome", + surface = surface, + target = { x = position.x - 21, y = position.y - 15 }, + color = tcolor, + scale = 20, + font = "compi", + time_to_live = ttl, + -- players={player}, + draw_on_ground = true, + orientation = 0, + -- alignment=center, + scale_with_zoom = false, + only_in_alt_mode = false } + local rid2 = rendering.draw_text { text = "Home", + surface = surface, + target = { x = position.x - 14, y = position.y - 5 }, + color = tcolor, + scale = 20, + font = "compi", + time_to_live = ttl, + -- players={player}, + draw_on_ground = true, + orientation = 0, + -- alignment=center, + scale_with_zoom = false, + only_in_alt_mode = false } table.insert(global.oarc_renders_fadeout, rid1) table.insert(global.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, --- It no LONGER generates the resources though as that is now handled in a delayed event! +---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) - for name,spawn in pairs(global.ocore.uniqueSpawns) do - - -- Create a bunch of useful area and position variables - local landArea = GetAreaAroundPos(spawn.pos, global.ocfg.spawn_config.gen_settings.land_area_tiles+CHUNK_SIZE) - -- local safeArea = GetAreaAroundPos(spawn.pos, global.ocfg.spawn_config.safe_area.safe_radius) - -- local warningArea = GetAreaAroundPos(spawn.pos, global.ocfg.spawn_config.safe_area.warn_radius) - -- local reducedArea = GetAreaAroundPos(spawn.pos, global.ocfg.spawn_config.safe_area.danger_radius) - local chunkAreaCenter = {x=chunkArea.left_top.x+(CHUNK_SIZE/2), - y=chunkArea.left_top.y+(CHUNK_SIZE/2)} - local spawnPosOffset = {x=spawn.pos.x+global.ocfg.spawn_config.gen_settings.land_area_tiles, - y=spawn.pos.y+global.ocfg.spawn_config.gen_settings.land_area_tiles} - - - - -- Make chunks near a spawn safe by removing enemies - if (getDistance(spawn.pos, chunkAreaCenter) < global.ocfg.spawn_config.safe_area.safe_radius) then - RemoveAliensInArea(surface, chunkArea) - - -- Create a warning area with heavily reduced enemies - elseif (getDistance(spawn.pos, chunkAreaCenter) < global.ocfg.spawn_config.safe_area.warn_radius) then - ReduceAliensInArea(surface, chunkArea, global.ocfg.spawn_config.safe_area.warn_reduction) - -- DowngradeWormsInArea(surface, chunkArea, 100, 100, 100) - RemoveWormsInArea(surface, chunkArea, false, true, true, true) -- remove all non-small worms. - - -- Create a third area with moderatly reduced enemies - elseif (getDistance(spawn.pos, chunkAreaCenter) < global.ocfg.spawn_config.safe_area.danger_radius) then - ReduceAliensInArea(surface, chunkArea, global.ocfg.spawn_config.safe_area.danger_reduction) - -- DowngradeWormsInArea(surface, chunkArea, 50, 100, 100) - RemoveWormsInArea(surface, chunkArea, false, false, true, true) -- remove all huge/behemoth worms. - end - if (not spawn.vanilla) then - -- 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) - if CheckIfInArea(chunkAreaCenter,landArea) then + local closest_spawn = GetClosestUniqueSpawn(surface.name, chunkArea.left_top) + if (closest_spawn == nil) then return end - -- Remove trees/resources inside the spawn area - RemoveInCircle(surface, chunkArea, "tree", spawn.pos, global.ocfg.spawn_config.gen_settings.land_area_tiles) - RemoveInCircle(surface, chunkArea, "resource", spawn.pos, global.ocfg.spawn_config.gen_settings.land_area_tiles+5) - RemoveInCircle(surface, chunkArea, "cliff", spawn.pos, global.ocfg.spawn_config.gen_settings.land_area_tiles+5) + --[[@type OarcConfigSpawnGeneral]] + local general_spawn_config = global.ocfg.spawn_general - local fill_tile = "landfill" - if (game.active_mods["oarc-restricted-build"]) then - fill_tile = global.ocfg.locked_build_area_tile - end + local chunkAreaCenter = { + x = chunkArea.left_top.x + (CHUNK_SIZE / 2), + y = chunkArea.left_top.y + (CHUNK_SIZE / 2) + } - if (global.ocfg.spawn_config.gen_settings.tree_circle) then - CreateCropCircle(surface, spawn.pos, chunkArea, global.ocfg.spawn_config.gen_settings.land_area_tiles, fill_tile) - end - if (global.ocfg.spawn_config.gen_settings.tree_octagon) then - CreateCropOctagon(surface, spawn.pos, chunkArea, global.ocfg.spawn_config.gen_settings.land_area_tiles, fill_tile) - end - if (global.ocfg.spawn_config.gen_settings.moat_choice_enabled) then - if (spawn.moat) then - CreateMoat(surface, - spawn.pos, - chunkArea, - global.ocfg.spawn_config.gen_settings.land_area_tiles, - "water", - global.ocfg.spawn_config.gen_settings.moat_bridging) - end - end - end + -- 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, global.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, general_spawn_config.spawn_radius_tiles + 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"}, spawn.position, general_spawn_config.spawn_radius_tiles + 5) + elseif (general_spawn_config.shape == SPAWN_SHAPE_CHOICE_SQUARE) then + RemoveInSquare(surface, chunkArea, {"resource", "cliff", "tree"}, spawn.position, general_spawn_config.spawn_radius_tiles + 5) + end + + -- Fill in the spawn area with landfill and create a circle of trees around it. + local fill_tile = "landfill" + if general_spawn_config.force_grass then + fill_tile = "grass-1" end + + if (general_spawn_config.shape == SPAWN_SHAPE_CHOICE_CIRCLE) then + CreateCropCircle( + surface, + spawn.position, + chunkArea, + general_spawn_config.spawn_radius_tiles, + fill_tile, + spawn.moat, + global.ocfg.gameplay.enable_moat_bridging + ) + elseif (general_spawn_config.shape == SPAWN_SHAPE_CHOICE_OCTAGON) then + CreateCropOctagon( + surface, + spawn.position, + chunkArea, + general_spawn_config.spawn_radius_tiles, + fill_tile, + spawn.moat, + global.ocfg.gameplay.enable_moat_bridging + ) + elseif (general_spawn_config.shape == SPAWN_SHAPE_CHOICE_SQUARE) then + CreateCropSquare( + surface, + spawn.position, + chunkArea, + general_spawn_config.spawn_radius_tiles, + fill_tile, + spawn.moat, + global.ocfg.gameplay.enable_moat_bridging + ) + end + + :: CONTINUE :: end end --- This is the main function that creates the spawn area --- Provides resources, land and a safe zone +---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 - -- Modify enemies first. - if global.ocfg.modified_enemy_spawning then - DowngradeWormsDistanceBasedOnChunkGenerate(event) - end + -- Don't block based on spawn enabled. + -- if (not global.oarc_surfaces[surface.name]) then return end -- Downgrade resources near to spawns - if global.ocfg.scale_resources_around_spawns then + -- TODO: Space Age will change this! + if global.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 does a few things like make the area - -- safe and provide a guaranteed area of land and water tiles. + -- 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. +---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(chunkArea.left_top) + local closestSpawn = GetClosestUniqueSpawn(surface.name, chunkArea.left_top) if (closestSpawn == nil) then return end - local distance = getDistance(chunkArea.left_top, closestSpawn.pos) + local distance = util.distance(chunkArea.left_top, closestSpawn.position) -- Adjust multiplier to bring it in or out - local modifier = (distance / (global.ocfg.spawn_config.safe_area.danger_radius*1))^3 + local modifier = (distance / (global.ocfg.surfaces_config[surface.name].spawn_config.safe_area.danger_radius * 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 key, entity in pairs(surface.find_entities_filtered{area=chunkArea, type="resource"}) do - if entity.valid and entity and entity.position and entity.amount then + 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() @@ -471,665 +641,795 @@ function DowngradeResourcesDistanceBasedOnChunkGenerate(surface, chunkArea) else entity.amount = new_amount end - end - end - end -end - --- I wrote this to ensure everyone gets safer spawns regardless of evolution level. --- This is intended to downgrade any biters/spitters spawning near player bases. --- I'm not sure the performance impact of this but I'm hoping it's not bad. -function ModifyEnemySpawnsNearPlayerStartingAreas(event) - - if (not event.entity or not (event.entity.force.name == "enemy") or not event.entity.position) then - log("ModifyBiterSpawns - Unexpected use.") - return - end - - local enemy_pos = event.entity.position - local surface = event.entity.surface - local enemy_name = event.entity.name - - local closest_spawn = GetClosestUniqueSpawn(enemy_pos) - - if (closest_spawn == nil) then - -- log("GetClosestUniqueSpawn ERROR - None found?") - return - end - - -- No enemies inside safe radius! - if (getDistance(enemy_pos, closest_spawn.pos) < global.ocfg.spawn_config.safe_area.safe_radius) then - event.entity.destroy() - - -- Warn distance is all SMALL only. - elseif (getDistance(enemy_pos, closest_spawn.pos) < global.ocfg.spawn_config.safe_area.warn_radius) then - if ((enemy_name == "big-biter") or (enemy_name == "behemoth-biter") or (enemy_name == "medium-biter")) then - event.entity.destroy() - surface.create_entity{name = "small-biter", position = enemy_pos, force = game.forces.enemy} - -- log("Downgraded biter close to spawn.") - elseif ((enemy_name == "big-spitter") or (enemy_name == "behemoth-spitter") or (enemy_name == "medium-spitter")) then - event.entity.destroy() - surface.create_entity{name = "small-spitter", position = enemy_pos, force = game.forces.enemy} - -- log("Downgraded spitter close to spawn.") - elseif ((enemy_name == "big-worm-turret") or (enemy_name == "behemoth-worm-turret") or (enemy_name == "medium-worm-turret")) then - event.entity.destroy() - surface.create_entity{name = "small-worm-turret", position = enemy_pos, force = game.forces.enemy} - -- log("Downgraded worm close to spawn.") - end - - -- Danger distance is MEDIUM max. - elseif (getDistance(enemy_pos, closest_spawn.pos) < global.ocfg.spawn_config.safe_area.danger_radius) then - if ((enemy_name == "big-biter") or (enemy_name == "behemoth-biter")) then - event.entity.destroy() - surface.create_entity{name = "medium-biter", position = enemy_pos, force = game.forces.enemy} - -- log("Downgraded biter further from spawn.") - elseif ((enemy_name == "big-spitter") or (enemy_name == "behemoth-spitter")) then - event.entity.destroy() - surface.create_entity{name = "medium-spitter", position = enemy_pos, force = game.forces.enemy} - -- log("Downgraded spitter further from spawn - elseif ((enemy_name == "big-worm-turret") or (enemy_name == "behemoth-worm-turret")) then - event.entity.destroy() - surface.create_entity{name = "medium-worm-turret", position = enemy_pos, force = game.forces.enemy} - -- log("Downgraded worm further from spawn.") + end end end end - - --[[ - ___ _ ___ _ _ _ _ _ ___ + ___ _ ___ _ _ _ _ _ ___ / __|| | | __| /_\ | \| || | | || _ \ | (__ | |__ | _| / _ \ | .` || |_| || _/ - \___||____||___|/_/ \_\|_|\_| \___/ |_| - ---]] - - -function ResetPlayerAndDestroyForce(player) - local player_old_force = player.force - - player.force = global.ocfg.main_force + \___||____||___|/_/ \_\|_|\_| \___/ |_| - if ((#player_old_force.players == 0) and (player_old_force.name ~= global.ocfg.main_force)) then - SendBroadcastMsg("Team " .. player_old_force.name .. " has been destroyed! All buildings will slowly be destroyed now.") - log("DestroyForce - FORCE DESTROYED: " .. player_old_force.name) - game.merge_forces(player_old_force, global.ocore.destroyed_force) - end - - RemoveOrResetPlayer(player, false, false, true, true) - SeparateSpawnsPlayerCreated(player.index, false) -end - -function ResetPlayerAndAbandonForce(player) - local player_old_force = player.force - - player.force = global.ocfg.main_force - - if ((#player_old_force.players == 0) and (player_old_force.name ~= global.ocfg.main_force)) then - SendBroadcastMsg("Team " .. player_old_force.name .. " has been abandoned!") - log("AbandonForce - FORCE ABANDONED: " .. player_old_force.name) - game.merge_forces(player_old_force, global.ocore.abandoned_force) - end - - RemoveOrResetPlayer(player, false, false, false, false) - SeparateSpawnsPlayerCreated(player.index, false) -end - -function ResetPlayerAndMergeForceToNeutral(player) - RemoveOrResetPlayer(player, false, true, true, true) - SeparateSpawnsPlayerCreated(player.index, true) -end - -function KickAndMarkPlayerForRemoval(player) - game.kick_player(player, "KickAndMarkPlayerForRemoval") - if (not global.ocore.player_removal_list) then - global.ocore.player_removal_list = {} - end - table.insert(global.ocore.player_removal_list, player) -end +--]] --- Call this if a player leaves the game early (or a player wants an early game reset) -function RemoveOrResetPlayer(player, remove_player, remove_force, remove_base, immediate) +-- ---Resets the player and destroys their force if they are not on the main one. +-- ---@param player LuaPlayer +-- ---@return nil +-- function ResetPlayerAndDestroyForce(player) +-- local player_old_force = player.force + +-- player.force = global.ocfg.gameplay.main_force_name + +-- if ((#player_old_force.players == 0) and (player_old_force.name ~= global.ocfg.gameplay.main_force_name)) then +-- SendBroadcastMsg("Team " .. +-- player_old_force.name .. " has been destroyed! All buildings will slowly be destroyed now.") --: localize +-- log("DestroyForce - FORCE DESTROYED: " .. player_old_force.name) +-- game.merge_forces(player_old_force, DESTROYED_FORCE_NAME) +-- end + +-- RemoveOrResetPlayer(player, false, false, true, true) +-- SeparateSpawnsInitPlayer(player.index) +-- end + +-- ---Resets the player and merges their force into the abandoned_force. +-- ---@param player LuaPlayer +-- ---@return nil +-- function ResetPlayerAndAbandonForce(player) +-- local player_old_force = player.force + +-- player.force = global.ocfg.gameplay.main_force_name + +-- if ((#player_old_force.players == 0) and (player_old_force.name ~= global.ocfg.gameplay.main_force_name)) then +-- SendBroadcastMsg("Team " .. player_old_force.name .. " has been abandoned!") --: localize +-- log("AbandonForce - FORCE ABANDONED: " .. player_old_force.name) +-- game.merge_forces(player_old_force, ABANDONED_FORCE_NAME) +-- end + +-- RemoveOrResetPlayer(player, false, false, false, false) +-- SeparateSpawnsInitPlayer(player.index) +-- end + +-- ---Reset player and merge their force to neutral +-- ---@param player LuaPlayer +-- ---@return nil +-- function ResetPlayerAndMergeForceToNeutral(player) +-- RemoveOrResetPlayer(player, false, true, true, true) +-- SeparateSpawnsInitPlayer(player.index) +-- 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 < (global.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. - player.teleport({x=0,y=0}, GAME_SURFACE_NAME) + player.teleport({x=0,y=0}, HOLDING_PEN_SURFACE_NAME) local player_old_force = player.force - player.force = global.ocfg.main_force + player.force = global.ocfg.gameplay.main_force_name -- Clear globals - CleanupPlayerGlobals(player.name) -- Except global.ocore.uniqueSpawns + CleanupPlayerGlobals(player.name) -- This cleans global.unique_spawns IF we are transferring ownership. - -- Clear their unique spawn (if they have one) - UniqueSpawnCleanupRemove(player.name, remove_base, immediate) -- Specifically global.ocore.uniqueSpawns + -- Safely clear the unique spawn IF it is still valid. + UniqueSpawnCleanupRemove(player.name) -- Specifically global.unique_spawns -- Remove a force if this player created it and they are the only one on it - if (remove_force) then - if ((#player_old_force.players == 0) and (player_old_force.name ~= global.ocfg.main_force)) then - log("RemoveOrResetPlayer - FORCE REMOVED: " .. player_old_force.name) - game.merge_forces(player_old_force, "neutral") - end + if ((#player_old_force.players == 0) and (player_old_force.name ~= global.ocfg.gameplay.main_force_name)) then + log("RemoveOrResetPlayer - FORCE REMOVED: " .. player_old_force.name) + game.merge_forces(player_old_force, "neutral") end -- Remove the character completely if (remove_player) then - game.remove_offline_players({player}) + game.remove_offline_players({ player }) end end -function UniqueSpawnCleanupRemove(playerName, cleanup, immediate) - if (global.ocore.uniqueSpawns[playerName] == nil) then return end -- Safety - log("UniqueSpawnCleanupRemove - " .. playerName) - - local spawnPos = global.ocore.uniqueSpawns[playerName].pos - - -- Check if it was near someone else's base. (Really just buddy base is possible I think.) - nearOtherSpawn = false - for spawnPlayerName,otherSpawnPos in pairs(global.ocore.uniqueSpawns) do - if ((spawnPlayerName ~= playerName) and (getDistance(spawnPos, otherSpawnPos.pos) < (global.ocfg.spawn_config.gen_settings.land_area_tiles*3))) then - log("Won't remove base as it's close to another spawn: " .. spawnPlayerName) - nearOtherSpawn = true +---Searches all unique spawns for the primary one for a player. +---@param player_name string +---@return OarcUniqueSpawn? +function FindPrimaryUniqueSpawn(player_name) + for _,spawns in pairs(global.unique_spawns) do + if (spawns[player_name] ~= nil and spawns[player_name].primary) then + return spawns[player_name] end end +end - -- Unused Chunk Removal mod (aka regrowth) - if (cleanup and global.ocfg.enable_abandoned_base_removal and (not nearOtherSpawn) and global.ocfg.enable_regrowth) then - - if (global.ocore.uniqueSpawns[playerName].vanilla) then - log("Returning a vanilla spawn back to available.") - table.insert(global.vanillaSpawns, {x=spawnPos.x,y=spawnPos.y}) - end - - if (immediate) then - log("IMMEDIATE Removing base: " .. spawnPos.x .. "," .. spawnPos.y) - RegrowthMarkAreaForRemoval(spawnPos, math.ceil(global.ocfg.spawn_config.gen_settings.land_area_tiles/CHUNK_SIZE)) - TriggerCleanup() - else - log("Removing permanent flags on base: " .. spawnPos.x .. "," .. spawnPos.y) - RegrowthMarkAreaNotPermanentOVERWRITE(spawnPos, math.ceil(global.ocfg.spawn_config.gen_settings.land_area_tiles/CHUNK_SIZE)) +---Searches all unique spawns for a list of secondary ones for a player. +---@param player_name string +---@return table -- Indexed by surface name! +function FindSecondaryUniqueSpawns(player_name) + local secondary_spawns = {} + for surface_index, spawns in pairs(global.unique_spawns) do + if (spawns[player_name] ~= nil and not spawns[player_name].primary) then + secondary_spawns[surface_index] = spawns[player_name] end end - - global.ocore.uniqueSpawns[playerName] = nil + return secondary_spawns end -function CleanupPlayerGlobals(playerName) +---Cleans up a player's unique spawn point, if safe to do so. +---@param player_name string +---@return nil +function UniqueSpawnCleanupRemove(player_name) - -- Clear the buddy pair IF one exists - if (global.ocore.buddyPairs[playerName] ~= nil) then - local buddyName = global.ocore.buddyPairs[playerName] - global.ocore.buddyPairs[playerName] = nil - global.ocore.buddyPairs[buddyName] = nil - end + -- Assumes we only remove the one primary unique spawn per player. + local primary_spawn = FindPrimaryUniqueSpawn(player_name) + if (primary_spawn == nil) then return end -- Safety + log("UniqueSpawnCleanupRemove - " .. player_name) - -- Remove them from the buddy waiting list - for idx,name in pairs(global.ocore.waitingBuddies) do - if (name == playerName) then - table.remove(global.ocore.waitingBuddies, idx) - break + local total_spawn_width = global.ocfg.spawn_general.spawn_radius_tiles + + global.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(global.unique_spawns[primary_spawn.surface_name]) do + if ((player_index ~= player_name) and + (util.distance(primary_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 - -- Clear buddy spawn options (should already be cleared, but just in case it isn't) - if (global.ocore.buddySpawnOpts[playerName] ~= nil) then - global.ocore.buddySpawnOpts[playerName] = nil + -- Use regrowth mod to cleanup the area. + local spawn_position = primary_spawn.position + if (global.ocfg.regrowth.enable_abandoned_base_cleanup and (not nearOtherSpawn)) then + log("Removing base: " .. spawn_position.x .. "," .. spawn_position.y .. " on surface: " .. primary_spawn.surface_name) + RegrowthMarkAreaForRemoval(primary_spawn.surface_name, spawn_position, math.ceil(total_spawn_width / CHUNK_SIZE) + 1) -- +1 to match the spawn generation requested area + TriggerCleanup() end - -- Transfer or remove a shared spawn if player is owner - if (global.ocore.sharedSpawns[playerName] ~= nil) then - - local teamMates = global.ocore.sharedSpawns[playerName].players + global.unique_spawns[primary_spawn.surface_name][player_name] = nil +end - if (#teamMates >= 1) then - local newOwnerName = table.remove(teamMates) -- Remove 1 to use as new owner. - TransferOwnershipOfSharedSpawn(playerName, newOwnerName) - SendBroadcastMsg(playerName .. "has left so " .. newOwnerName .. " now owns their base.") - else - global.ocore.sharedSpawns[playerName] = 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 (global.buddy_pairs[player_name] ~= nil) then + local buddyName = global.buddy_pairs[player_name] + global.buddy_pairs[player_name] = nil + global.buddy_pairs[buddyName] = nil end - -- Remove from other shared spawns (need to search all) - for _,sharedSpawn in pairs(global.ocore.sharedSpawns) do - for key,name in pairs(sharedSpawn.players) do - if (playerName == name) then - sharedSpawn.players[key] = nil; - goto LOOP_BREAK -- Nest loop break. + -- 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. + TransferOwnershipOfSharedSpawn(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(global.unique_spawns) do + for player_index, spawn in pairs(spawns) do + for index, joiner in pairs(spawn.joiners) do + if (player_name == joiner) then + global.unique_spawns[surface_index][player_index].joiners[index] = nil + goto LOOP_BREAK -- Nest loop break. Assumes only one entry per player is possible. + end end end end ::LOOP_BREAK:: -- Clear their personal spawn point info - if (global.ocore.playerSpawns[playerName] ~= nil) then - global.ocore.playerSpawns[playerName] = nil + if (global.player_respawns[player_name] ~= nil) then + global.player_respawns[player_name] = nil end -- Remove them from the delayed spawn queue if they are in it - for idx,delayedSpawn in pairs(global.ocore.delayedSpawns) do - if (playerName == delayedSpawn.playerName) then - if (delayedSpawn.vanilla) then - log("Returning a vanilla spawn back to available.") - table.insert(global.vanillaSpawns, {x=delayedSpawn.pos.x,y=delayedSpawn.pos.y}) - end - - table.remove(global.ocore.delayedSpawns, idx) - log("Removing player from delayed spawn queue: " .. playerName) + for index, delayedSpawn in pairs(global.delayed_spawns --[[@as OarcDelayedSpawnsTable]]) do + if (player_name == delayedSpawn.playerName) then + global.delayed_spawns[index] = nil + log("Removing player from delayed spawn queue: " .. player_name) break end end - if (global.ocore.playerCooldowns[playerName] ~= nil) then - global.ocore.playerCooldowns[playerName] = nil - end + -- Remove them from any join queues they may be in: + RemovePlayerFromJoinQueue(player_name) - global.oarc_store.pmf_counts[playerName] = {} + if (global.player_cooldowns[player_name] ~= nil) then + global.player_cooldowns[player_name] = nil + end end -function TransferOwnershipOfSharedSpawn(prevOwnerName, newOwnerName) - -- Transfer the shared spawn global - global.ocore.sharedSpawns[newOwnerName] = global.ocore.sharedSpawns[prevOwnerName] - global.ocore.sharedSpawns[newOwnerName].openAccess = false - global.ocore.sharedSpawns[prevOwnerName] = nil - - -- Transfer the unique spawn global - global.ocore.uniqueSpawns[newOwnerName] = global.ocore.uniqueSpawns[prevOwnerName] - global.ocore.uniqueSpawns[prevOwnerName] = nil - - game.players[newOwnerName].print("You have been given ownership of this base!") +---Transfers ownership of a shared spawn to another player. +---@param spawn OarcUniqueSpawn +---@param new_host_name string +---@return nil +function TransferOwnershipOfSharedSpawn(spawn, new_host_name) + + -- Create a new unique for the new owner based on the old one. + global.unique_spawns[spawn.surface_name][new_host_name] = { + position = spawn.position, + surface_name = spawn.surface_name, + primary = spawn.primary, + moat = spawn.moat, + host_name = new_host_name, + joiners = spawn.joiners, + join_queue = {}, + open_access = false, + buddy_name = spawn.buddy_name + } + + -- Update the matching buddy spawn if it exists. + if (spawn.buddy_name ~= nil) then + global.unique_spawns[spawn.surface_name][spawn.buddy_name].buddy_name = new_host_name + end + + -- Delete the old one + global.unique_spawns[spawn.surface_name][spawn.host_name] = nil + + game.players[new_host_name].print({ "oarc-new-owner-msg" }) end --[[ - _ _ ___ _ ___ ___ ___ ___ _____ _ _ ___ ___ + _ _ ___ _ ___ ___ ___ ___ _____ _ _ ___ ___ | || || __|| | | _ \| __|| _ \ / __||_ _|| | | || __|| __| - | __ || _| | |__ | _/| _| | / \__ \ | | | |_| || _| | _| - |_||_||___||____||_| |___||_|_\ |___/ |_| \___/ |_| |_| - + | __ || _| | |__ | _/| _| | / \__ \ | | | |_| || _| | _| + |_||_||___||____||_| |___||_|_\ |___/ |_| \___/ |_| |_| + --]] --- Same as GetClosestPosFromTable but specific to global.ocore.uniqueSpawns -function GetClosestUniqueSpawn(pos) +---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(global.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 + global.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 global.unique_spawns +---@param surface_name string +---@param pos MapPosition +---@return OarcUniqueSpawn? +function GetClosestUniqueSpawn(surface_name, pos) + + local surface_spawns + for surface_index, spawns in pairs(global.unique_spawns) do + if (surface_index == surface_name) then + if (table_size(spawns) == 0) then return nil end -- EXIT - No spawns on requested surface + surface_spawns = spawns + end + end + if (surface_spawns == nil) then return nil end local closest_dist = nil - local closest_key = nil + local closest_spawn = nil - for k,s in pairs(global.ocore.uniqueSpawns) do - local new_dist = getDistance(pos, s.pos) + 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_key = k + closest_spawn = spawn elseif (closest_dist > new_dist) then closest_dist = new_dist - closest_key = k + closest_spawn = spawn end end - if (closest_key == nil) then - -- log("GetClosestUniqueSpawn ERROR - None found?") - return nil - end - - return global.ocore.uniqueSpawns[closest_key] + return closest_spawn end --- Return the owner of the shared spawn for this player. --- May return nil if player has not spawned yet. -function FindPlayerSharedSpawn(playerName) +---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 = {} - -- If the player IS an owner, he can't be in any other shared base. - if (global.ocore.sharedSpawns[playerName] ~= nil) then - return playerName - end + for _, spawns in pairs(global.unique_spawns) do + for _,spawn in pairs(spawns) do + if (not spawn.primary) then goto CONTINUE end - -- Otherwise, search all shared spawns for this player and return the owner. - for ownerName,sharedSpawn in pairs(global.ocore.sharedSpawns) do - for _,sharingPlayerName in pairs(sharedSpawn.players) do - if (playerName == sharingPlayerName) then - return ownerName + -- 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 - -- Lastly, return nil if not found. Means player hasn't been assigned a base yet. - return nil + return shared_players end --- Returns the number of players currently online at the shared spawn -function GetOnlinePlayersAtSharedSpawn(ownerName) - if (global.ocore.sharedSpawns[ownerName] ~= nil) then - -- Does not count base owner - local count = 0 - -- For each player in the shared spawn, check if online and add to count. - for _,player in pairs(game.connected_players) do - if (ownerName == player.name) then - count = count + 1 - 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 = global.unique_spawns[surface_name][owner_name] - for _,playerName in pairs(global.ocore.sharedSpawns[ownerName].players) do + if spawn == nil then return 0 end - if (playerName == player.name) then - count = count + 1 - end - 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 - return count - else - return 0 + -- Add the host player to the count + if game.players[owner_name].connected then + count = count + 1 end -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. -function GetNumberOfAvailableSharedSpawns() - local count = 0 + return count +end - for ownerName,sharedSpawn in pairs(global.ocore.sharedSpawns) do - if (sharedSpawn.openAccess and - (game.players[ownerName] ~= nil) and - game.players[ownerName].connected) then - if ((global.ocfg.max_players_shared_spawn == 0) or - (#global.ocore.sharedSpawns[ownerName].players < global.ocfg.max_players_shared_spawn)) then - count = count+1 +-- -- 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(global.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 count + return available_spawns end -function DoesPlayerHaveCustomSpawn(player) - for name,spawnPos in pairs(global.ocore.playerSpawns) do - if (player.name == name) then - return true - 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 (global.unique_spawns[surface_name] == nil) or (global.unique_spawns[surface_name][owner_name] == nil) then + return false end - return false + + local spawn = global.unique_spawns[surface_name][owner_name] + + if (not spawn.open_access) then + return false + end + + if (game.players[owner_name] == nil) or not (game.players[owner_name].connected) then + return false + end + + return true end -function ChangePlayerSpawn(player, pos) - global.ocore.playerSpawns[player.name] = pos - global.ocore.playerCooldowns[player.name] = {setRespawn=game.tick} +---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 (global.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) >= global.ocfg.gameplay.number_of_players_per_shared_spawn) end -function QueuePlayerForDelayedSpawn(playerName, spawn, moatEnabled, vanillaSpawn) +-- ---Checks if player has a custom spawn point set. +-- ---@param player LuaPlayer +-- ---@return boolean +-- function DoesPlayerHaveCustomSpawn(player) +-- for name,_ in pairs(global.player_respawns --[[@as OarcPlayerRespawnsTable]]) do +-- if (player.name == name) then +-- return true +-- end +-- end +-- return false +-- end + +-- ---Gets the custom spawn point for a player if they have one. +-- ---@param player LuaPlayer +-- ---@return OarcPlayerSpawn? +-- function GetPlayerCustomSpawn(player) +-- for name, player_spawn in pairs(global.player_respawns --[[@as OarcPlayerRespawnsTable]]) do +-- if (player.name == name) then +-- return player_spawn +-- end +-- end +-- return nil +-- 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 + + global.player_respawns[player_name][surface_name] = updatedPlayerSpawn + + + if (global.player_cooldowns[player_name].setRespawn == nil) or reset_cooldown then + global.player_cooldowns[player_name].setRespawn = game.tick + end +end + +---Creates the global.unique_spawns entries for a new spawn area. +---@param player_name string +---@param surface_name string +---@param spawn_position MapPosition +---@param moat_enabled boolean +---@param primary boolean +---@param buddy_name string? +---@return nil +function InitUniqueSpawnGlobals(player_name, surface_name, spawn_position, moat_enabled, primary, buddy_name) + + ---@type OarcUniqueSpawn + local new_unique_spawn = { + surface_name = surface_name, + position = spawn_position, + moat = moat_enabled, + primary = primary, + host_name = player_name, + joiners = {}, + join_queue = {}, + open_access = false, + buddy_name = buddy_name + } + + if global.unique_spawns[surface_name] == nil then + global.unique_spawns[surface_name] = {} + end + + global.unique_spawns[surface_name][player_name] = new_unique_spawn +end +---Queue a player for a delayed spawn. This will generate the spawn area and move the player there when ready. +---@param player_name string +---@param surface string +---@param spawn_position MapPosition +---@param moat_enabled boolean +---@param primary boolean +---@param buddy_name string? +---@return nil +function QueuePlayerForDelayedSpawn(player_name, surface, spawn_position, moat_enabled, primary, buddy_name) -- If we get a valid spawn point, setup the area - if ((spawn.x ~= 0) or (spawn.y ~= 0)) then - global.ocore.uniqueSpawns[playerName] = {pos=spawn,moat=moatEnabled,vanilla=vanillaSpawn} + if ((spawn_position.x == 0) and (spawn_position.y == 0)) then + error("Invalid spawn position for player: " .. player_name .. " on surface: " .. surface) + end - local delay_spawn_seconds = 5*(math.ceil(global.ocfg.spawn_config.gen_settings.land_area_tiles/CHUNK_SIZE)) + InitUniqueSpawnGlobals(player_name, surface, spawn_position, moat_enabled, primary, buddy_name) - game.players[playerName].print("Generating your spawn now, please wait for at least " .. delay_spawn_seconds .. " seconds...") - game.players[playerName].surface.request_to_generate_chunks(spawn, 4) - delayedTick = game.tick + delay_spawn_seconds*TICKS_PER_SECOND - table.insert(global.ocore.delayedSpawns, {playerName=playerName, pos=spawn, moat=moatEnabled, vanilla=vanillaSpawn, delayedTick=delayedTick}) + -- Add a 1 chunk buffer to be safe + local total_spawn_width = global.ocfg.spawn_general.spawn_radius_tiles + + global.ocfg.spawn_general.moat_width_tiles + local spawn_chunk_radius = math.ceil(total_spawn_width / CHUNK_SIZE) + 1 + local delay_spawn_seconds = 5 * spawn_chunk_radius - HideOarcGui(game.players[playerName]) - HideOarcStore(game.players[playerName]) - DisplayPleaseWaitForSpawnDialog(game.players[playerName], delay_spawn_seconds) + game.players[player_name].print({ "oarc-generating-spawn-please-wait" }) + game.surfaces[surface].request_to_generate_chunks(spawn_position, spawn_chunk_radius) - RegrowthMarkAreaSafeGivenTilePos(spawn, math.ceil(global.ocfg.spawn_config.gen_settings.land_area_tiles/CHUNK_SIZE), true) + local final_chunk = GetChunkPosFromTilePos(spawn_position) + final_chunk.x = final_chunk.x + spawn_chunk_radius + final_chunk.y = final_chunk.y + spawn_chunk_radius - else - log("THIS SHOULD NOT EVER HAPPEN! Spawn failed!") - SendBroadcastMsg("ERROR!! Failed to create spawn point for: " .. playerName) - end + ---@type OarcDelayedSpawn + local delayedSpawn = {} + delayedSpawn.playerName = player_name + delayedSpawn.surface = surface + delayedSpawn.position = spawn_position + delayedSpawn.moat = moat_enabled + delayedSpawn.delayedTick = game.tick + delay_spawn_seconds * TICKS_PER_SECOND + delayedSpawn.final_chunk_generated = final_chunk + + table.insert(global.delayed_spawns, delayedSpawn) + + HideOarcGui(game.players[player_name]) + DisplayPleaseWaitForSpawnDialog(game.players[player_name], delay_spawn_seconds, game.surfaces[surface], spawn_position) + + RegrowthMarkAreaSafeGivenTilePos(surface, spawn_position, + math.ceil(global.ocfg.spawn_general.spawn_radius_tiles / CHUNK_SIZE), true) + + -- Chart the area to be able to display the minimap while the player waits. + ChartArea(game.players[player_name].force, + delayedSpawn.position, + spawn_chunk_radius, + surface + ) end +---Creates and sends a player to a new secondary spawn, temporarily placing them in the holding pen. +---@param player LuaPlayer +---@param surface LuaSurface +---@return nil +function SecondarySpawn(player, surface) + + -- Ensure we still have their previous spawn choices + local spawn_choices = global.spawn_choices[player.name] + if (spawn_choices == nil) then + log("ERROR - SecondarySpawn - No spawn choices for player: " .. player.name) + return + end + + -- Confirm there is no existing spawn point for this player on this surface + if (global.unique_spawns[surface.name] ~= nil and global.unique_spawns[surface.name][player.name] ~= nil) then + log("ERROR - SecondarySpawn - Player already has a spawn point on this surface: " .. player.name) + return + end + + -- Find a new spawn point + local spawn_position = FindUngeneratedCoordinates(surface, 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 + player.print({ "oarc-no-ungenerated-land-error" }) + return + end + + -- Add new spawn point for the new surface + SetPlayerRespawn(player.name, surface.name, spawn_position, false) -- Do not reset cooldown + QueuePlayerForDelayedSpawn(player.name, surface.name, spawn_position, spawn_choices.moat, false, nil) + + -- Send them to the holding pen + SafeTeleport(player, game.surfaces[HOLDING_PEN_SURFACE_NAME], {x=0,y=0}) + + -- Announce + SendBroadcastMsg({"", { "oarc-player-new-secondary", player.name, surface.name }, " ", GetGPStext(surface.name, spawn_position)}) +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 ((global.ocore.delayedSpawns ~= nil) and (#global.ocore.delayedSpawns > 0)) then - for i=#global.ocore.delayedSpawns,1,-1 do - delayedSpawn = global.ocore.delayedSpawns[i] + if ((global.delayed_spawns ~= nil) and (#global.delayed_spawns > 0)) then - if (delayedSpawn.delayedTick < game.tick) then - -- TODO, add check here for if chunks around spawn are generated surface.is_chunk_generated(chunkPos) + -- I think this loop removes from the back of the table to the front?? + for i = #global.delayed_spawns, 1, -1 do + delayedSpawn = global.delayed_spawns[i] --[[@as OarcDelayedSpawn]] + + local surface = game.surfaces[delayedSpawn.surface] + + if ((delayedSpawn.delayedTick < game.tick) or surface.is_chunk_generated(delayedSpawn.final_chunk_generated) ) then if (game.players[delayedSpawn.playerName] ~= nil) then SendPlayerToNewSpawnAndCreateIt(delayedSpawn) end - table.remove(global.ocore.delayedSpawns, i) + table.remove(global.delayed_spawns, i) end end end end end -function SendPlayerToSpawn(player) - if (DoesPlayerHaveCustomSpawn(player)) then - SafeTeleport(player, - game.surfaces[GAME_SURFACE_NAME], - global.ocore.playerSpawns[player.name]) - else - SafeTeleport(player, - game.surfaces[GAME_SURFACE_NAME], - game.forces[global.ocfg.main_force].get_spawn_position(GAME_SURFACE_NAME)) - end +---Send player to their custom spawn point +---@param surface_name string +---@param player LuaPlayer +---@return nil +function SendPlayerToSpawn(surface_name, player) + local spawn = global.player_respawns[player.name][surface_name] + SafeTeleport(player, game.surfaces[surface_name], spawn.position) + player.permission_group = game.permissions.get_group("default") end -function SendPlayerToRandomSpawn(player) - local numSpawns = TableLength(global.ocore.uniqueSpawns) - local rndSpawn = math.random(0,numSpawns) - local counter = 0 +-- ---Send player to a random spawn point. +-- ---@param player LuaPlayer +-- ---@return nil +-- function SendPlayerToRandomSpawn(player) +-- local numSpawns = #global.oc--ore.unique--Spawns +-- local rndSpawn = math.random(0, numSpawns) +-- local counter = 0 + +-- if (rndSpawn == 0) then +-- local gameplayConfig = global.ocfg.gameplay --[[@as OarcConfigGameplaySettings]] +-- player.teleport( +-- game.forces[gameplayConfig.main_force_name].get_spawn_position(gameplayConfig.default_surface), +-- gameplayConfig.default_surface) +-- else +-- counter = counter + 1 +-- for name, spawn in pairs(global.oc--ore.unique--Spawns --[[@as OarcUnique--SpawnsTable]]) do +-- if (counter == rndSpawn) then +-- player.teleport(spawn.position) +-- break +-- end +-- counter = counter + 1 +-- end +-- end +-- end + +---Check if a player has a delayed spawn +---@param player_name string +---@return boolean +function PlayerHasDelayedSpawn(player_name) + for _,delayedSpawn in pairs(global.delayed_spawns --[[@as OarcDelayedSpawnsTable]]) do + if (delayedSpawn.playerName == player_name) then + return true + end + end + return false +end - if (rndSpawn == 0) then - player.teleport(game.forces[global.ocfg.main_force].get_spawn_position(GAME_SURFACE_NAME), GAME_SURFACE_NAME) - else - counter = counter + 1 - for name,spawn in pairs(global.ocore.uniqueSpawns) do - if (counter == rndSpawn) then - player.teleport(spawn.pos) - break - end - counter = counter + 1 +---Get the list of surfaces that are allowed for spawning. +---@return string[] +function GetAllowedSurfaces() + ---@type string[] + local surfaceList = {} + for surfaceName,allowed in pairs(global.oarc_surfaces --[[@as table]]) do + if allowed then + table.insert(surfaceList, surfaceName) end end + return surfaceList end --[[ - ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ + ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ | __|/ _ \ | _ \ / __|| __| / __|| _ \| __|/ __||_ _|| __||_ _|/ __| - | _|| (_) || /| (__ | _| \__ \| _/| _|| (__ | | | _| | || (__ + | _|| (_) || /| (__ | _| \__ \| _/| _|| (__ | | | _| | || (__ |_| \___/ |_|_\ \___||___| |___/|_| |___|\___||___||_| |___|\___| - + --]] -function CreateForce(force_name) - local newForce = nil + +---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 CreateForce(force_name .. "_") -- Append a character to make the force name unique. - - -- Create a new force - elseif (TableLength(game.forces) < MAX_FORCES) then - newForce = game.create_force(force_name) - if global.ocfg.enable_shared_team_vision then - newForce.share_chart = true - end - if global.ocfg.enable_research_queue then - newForce.research_queue_enabled = true - end - -- Chart silo areas if necessary - if global.ocfg.frontier_rocket_silo and global.ocfg.frontier_silo_vision then - ChartRocketSiloAreas(game.surfaces[GAME_SURFACE_NAME], newForce) - end - SetCeaseFireBetweenAllForces() - SetFriendlyBetweenAllForces() - newForce.friendly_fire = global.ocfg.enable_friendly_fire - if (global.ocfg.enable_anti_grief) then - AntiGriefing(newForce) - end - - if global.ocfg.lock_goodies_rocket_launch and not global.ocore.satellite_sent then - for _,v in ipairs(LOCKED_TECHNOLOGIES) do - DisableTech(newForce, v.t) - end - end + 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 = global.ocfg.gameplay.enable_shared_team_vision + new_force.friendly_fire = global.ocfg.gameplay.enable_friendly_fire + -- SetCeaseFireBetweenAllPlayerForces() + -- SetFriendlyBetweenAllPlayerForces() + ConfigurePlayerForceRelationships(true, true) + ConfigureEnemyForceRelationshipsForNewPlayerForce(new_force) else - log("TOO MANY FORCES!!! - CreateForce()") - return game.forces[global.ocfg.main_force] - end - - -- Add productivity bonus for solo teams. - if (ENABLE_FORCE_LAB_PROD_BONUS) then - local tech_mult = game.difficulty_settings.technology_price_multiplier - if (tech_mult > 1) and (force_name ~= global.ocfg.main_force) then - newForce.laboratory_productivity_bonus = (tech_mult-1) - end + log("TOO MANY FORCES!!! - CreatePlayerForce()") + return game.forces[global.ocfg.gameplay.main_force_name] end - -- Loot distance buff - newForce.character_loot_pickup_distance_bonus = 16 - - return newForce + 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 = CreateForce(player.name) + local newForce = CreatePlayerForce(player.name) player.force = newForce if (newForce.name == player.name) then - SendBroadcastMsg(player.name.." has started their own team!") + SendBroadcastMsg({ "oarc-player-started-own-team", player.name }) else - player.print("Sorry, no new teams can be created. You were assigned to the default team instead.") + player.print({ "oarc-player-no-new-teams-sorry" }) end return newForce end ---[[ - __ __ _ _ _ ___ _ _ _ ___ ___ _ __ __ _ _ ___ - \ \ / //_\ | \| ||_ _|| | | | /_\ / __|| _ \ /_\\ \ / /| \| |/ __| - \ V // _ \ | .` | | | | |__ | |__ / _ \ \__ \| _// _ \\ \/\/ / | .` |\__ \ - \_//_/ \_\|_|\_||___||____||____|/_/ \_\ |___/|_| /_/ \_\\_/\_/ |_|\_||___/ - ---]] --- Function to generate some map_gen_settings.starting_points --- You should only use this at the start of the game really. -function CreateVanillaSpawns(count, spacing) - - local points = {} - - -- Get an ODD number from the square of the input count. - -- Always rounding up so we don't end up with less points that requested. - local sqrt_count = math.ceil(math.sqrt(count)) - if (sqrt_count % 2 == 0) then - sqrt_count = sqrt_count + 1 - end - - -- Need to know how much to offset the grid. - local sqrt_half = math.floor((sqrt_count-1)/2) - - if (sqrt_count < 1) then - log("CreateVanillaSpawns less than 1!!") - return - end - - if (global.vanillaSpawns == nil) then - global.vanillaSpawns = {} - end - - -- This should give me points centered around 0,0 I think. - for i=-sqrt_half,sqrt_half,1 do - for j=-sqrt_half,sqrt_half,1 do - if (i~=0 or j~=0) then -- EXCEPT don't put 0,0 - - local x_pos = (i*spacing) - x_pos = x_pos - (x_pos % CHUNK_SIZE) + (CHUNK_SIZE/2) - local y_pos = (j*spacing) - y_pos = y_pos - (y_pos % CHUNK_SIZE) + (CHUNK_SIZE/2) - - table.insert(points, {x=x_pos,y=y_pos}) - table.insert(global.vanillaSpawns, {x=x_pos,y=y_pos}) - end - end - end - - -- Do something with the return value. - return points -end - --- Useful when combined with something like CreateVanillaSpawns --- Where it helps ensure ALL chunks generated use new map_gen_settings. -function DeleteAllChunksExceptCenter(surface) - -- Delete the starting chunks that make it into the game before settings are changed. - for chunk in surface.get_chunks() do - -- Don't delete the chunk that might contain players lol. - -- This is really only a problem for launching AS the host. Not headless - if ((chunk.x ~= 0) and (chunk.y ~= 0)) then - surface.delete_chunk({chunk.x, chunk.y}) - end - end -end - --- Find a vanilla spawn as close as possible to the given target_distance -function FindUnusedVanillaSpawn(surface, target_distance) - local best_key = nil - local best_distance = nil - - for k,v in pairs(global.vanillaSpawns) do - - -- Check if chunks nearby are not generated. - local chunk_pos = GetChunkPosFromTilePos(v) - if IsChunkAreaUngenerated(chunk_pos, CHECK_SPAWN_UNGENERATED_CHUNKS_RADIUS, surface) then - - -- Is this our first valid find? - if ((best_key == nil) or (best_distance == nil)) then - best_key = k - best_distance = math.abs(math.sqrt((v.x^2) + (v.y^2)) - target_distance) - - -- Check if it is closer to target_distance than previous option. - else - local new_distance = math.abs(math.sqrt((v.x^2) + (v.y^2)) - target_distance) - if (new_distance < best_distance) then - best_key = k - best_distance = new_distance - end - end - - -- If it's not a valid spawn anymore, let's remove it. - else - log("Removing vanilla spawn due to chunks generated: x=" .. v.x .. ",y=" .. v.y) - table.remove(global.vanillaSpawns, k) - end - end - - local spawn_pos = {x=0,y=0} - if ((best_key ~= nil) and (global.vanillaSpawns[best_key] ~= nil)) then - spawn_pos.x = global.vanillaSpawns[best_key].x - spawn_pos.y = global.vanillaSpawns[best_key].y - table.remove(global.vanillaSpawns, best_key) - end - log("Found unused vanilla spawn: x=" .. spawn_pos.x .. ",y=" .. spawn_pos.y) - return spawn_pos -end - - -function ValidateVanillaSpawns(surface) - for k,v in pairs(global.vanillaSpawns) do +--[[ - -- Check if chunks nearby are not generated. - local chunk_pos = GetChunkPosFromTilePos(v) - if not IsChunkAreaUngenerated(chunk_pos, CHECK_SPAWN_UNGENERATED_CHUNKS_RADIUS+15, surface) then - log("Removing vanilla spawn due to chunks generated: x=" .. v.x .. ",y=" .. v.y) - table.remove(global.vanillaSpawns, k) - end - end -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> + +---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. + +---Table of [OarcUniqueSpawnClass](lua://OarcUniqueSpawnClass) indexed first by surface name and then by player name. +---@alias OarcUniqueSpawnsTable table> + +---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 + +---Temporary data used when spawning a player. Player needs to wait while the area is prepared. +---@alias OarcDelayedSpawn { surface: string, playerName: string, position: MapPosition, moat: boolean, delayedTick: number, final_chunk_generated: ChunkPosition } +---Table of [OarcDelayedSpawn](lua://OarcDelayedSpawn) indexed by player name. +---@alias OarcDelayedSpawnsTable table + +---This contains the spawn choices for a player in the spawn menu. +---@alias OarcSpawnChoices { surface: string, team: SpawnTeamChoice, moat: boolean, buddy: string?, distance: integer, host: string?, buddy_team: boolean } +---Table of [OarcSpawnChoices](lua://OarcSpawnChoices) indexed by player name. +---@alias OarcSpawnChoicesTable table diff --git a/lib/separate_spawns_guis.lua b/lib/separate_spawns_guis.lua index 0206907..452c569 100644 --- a/lib/separate_spawns_guis.lua +++ b/lib/separate_spawns_guis.lua @@ -1,89 +1,88 @@ --- separate_spawns_guis.lua --- Nov 2016 - --- I made a separate file for all the GUI related functions - -require("lib/separate_spawns") +-- I made a separate file for all the GUI related functions. Yay me. local SPAWN_GUI_MAX_WIDTH = 500 local SPAWN_GUI_MAX_HEIGHT = 1000 --- Use this for testing shared spawns... --- local sharedSpawnExample1 = {openAccess=true, --- position={x=50,y=50}, --- players={"ABC", "DEF"}} --- local sharedSpawnExample2 = {openAccess=false, --- position={x=200,y=200}, --- players={"ABC", "DEF"}} --- local sharedSpawnExample3 = {openAccess=true, --- position={x=400,y=400}, --- players={"A", "B", "C", "D"}} --- global.ocore.sharedSpawns = {testName1=sharedSpawnExample1, --- testName2=sharedSpawnExample2, --- Oarc=sharedSpawnExample3} - - --- A display gui message --- Meant to be display the first time a player joins. +---A display gui message. Meant to be display the first time a player joins. +---@param player LuaPlayer +---@return boolean function DisplayWelcomeTextGui(player) - if ((player.gui.screen["welcome_msg"] ~= nil) or - (player.gui.screen["spawn_opts"] ~= nil) or - (player.gui.screen["shared_spawn_opts"] ~= nil) or - (player.gui.screen["join_shared_spawn_wait_menu"] ~= nil) or - (player.gui.screen["buddy_spawn_opts"] ~= nil) or - (player.gui.screen["buddy_wait_menu"] ~= nil) or - (player.gui.screen["buddy_request_menu"] ~= nil) or - (player.gui.screen["wait_for_spawn_dialog"] ~= nil)) then + if ((player.gui.screen["join_shared_spawn_wait_menu"] ~= nil) or + (player.gui.screen["buddy_wait_menu"] ~= nil) or + (player.gui.screen["buddy_request_menu"] ~= nil) or + (player.gui.screen["wait_for_spawn_dialog"] ~= nil)) then log("DisplayWelcomeTextGui called while some other dialog is already displayed!") return false end - local wGui = player.gui.screen.add{name = "welcome_msg", - type = "frame", - direction = "vertical", - caption=global.ocfg.welcome_title} - wGui.auto_center=true - - wGui.style.maximal_width = SPAWN_GUI_MAX_WIDTH - wGui.style.maximal_height = SPAWN_GUI_MAX_HEIGHT + --Delete existing guis + if (player.gui.screen["welcome_msg"] ~= nil) then + player.gui.screen.welcome_msg.destroy() + end + if (player.gui.screen["spawn_opts"] ~= nil) then + player.gui.screen.spawn_opts.destroy() + end - -- Start with server message. - AddLabel(wGui, "server_msg_lbl1", global.ocfg.server_rules, my_label_style) - AddLabel(wGui, "contact_info_msg_lbl1", global.ocfg.server_contact, my_label_style) - AddSpacer(wGui) + local welcome_gui = player.gui.screen.add { + name = "welcome_msg", + type = "frame", + direction = "vertical", + caption = global.ocfg.server_info.welcome_msg_title + } + welcome_gui.auto_center = true + welcome_gui.style.maximal_width = SPAWN_GUI_MAX_WIDTH + welcome_gui.style.maximal_height = SPAWN_GUI_MAX_HEIGHT + welcome_gui.style.padding = 5 + + local welcome_gui_if = welcome_gui.add { + type = "frame", + style = "inside_shallow_frame_with_padding", + direction = "vertical" + } -- Informational message about the scenario - AddLabel(wGui, "scenario_info_msg_lbl1", SCENARIO_INFO_MSG, my_label_style) - AddSpacer(wGui) + if (global.ocfg.server_info.welcome_msg ~= " ") then + AddLabel(welcome_gui_if, nil, global.ocfg.server_info.welcome_msg, my_label_style) + AddSpacer(welcome_gui_if) + end - -- Warning about spawn creation time - AddLabel(wGui, "spawn_time_msg_lbl1", {"oarc-spawn-time-warning-msg"}, my_warning_style) + -- Warnings about the scenario + AddLabel(welcome_gui_if, nil, { "oarc-scenario-info-warn-msg" }, my_note_style) -- Confirm button - AddSpacerLine(wGui) - local button_flow = wGui.add{type = "flow"} - button_flow.style.horizontal_align = "right" + local button_flow = welcome_gui.add { + type = "flow", + style = "dialog_buttons_horizontal_flow", + } button_flow.style.horizontally_stretchable = true - button_flow.add{name = "welcome_okay_btn", - type = "button", - caption={"oarc-i-understand"}, - style = "confirm_button"} + + local dragger = button_flow.add { + type = "empty-widget", + style = "draggable_space_with_no_left_margin", + } + dragger.style.horizontally_stretchable = true + dragger.style.height = 30 + + local confirm_button = button_flow.add { + name = "welcome_okay_btn", + tags = { action = "oarc_spawn_options", setting = "welcome_okay" }, + type = "button", + caption = { "oarc-i-understand" }, + style = "confirm_button", + } + confirm_button.style.horizontal_align = "right" return true end - --- Handle the gui click of the welcome msg +---Handle the gui click of the welcome msg +---@param event EventData.on_gui_click +---@return nil function WelcomeTextGuiClick(event) - if not (event and event.element and event.element.valid) then return end + if not event.element.valid then return end local player = game.players[event.player_index] local buttonClicked = event.element.name - if not player then - log("Another gui click happened with no valid player...") - return - end - if (buttonClicked == "welcome_okay_btn") then if (player.gui.screen.welcome_msg ~= nil) then player.gui.screen.welcome_msg.destroy() @@ -92,1147 +91,1183 @@ function WelcomeTextGuiClick(event) end end +---Creates the spawn menu gui frame to hold all the spawn options. +---@param player LuaPlayer +---@return LuaGuiElement +function CreateSpawnMenuGuiFrame(player) + local spawn_opts_frame = player.gui.screen.add { + name = "spawn_opts", + type = "frame", + direction = "vertical", + caption = { "oarc-spawn-options" } + } + spawn_opts_frame.style.maximal_width = SPAWN_GUI_MAX_WIDTH + spawn_opts_frame.style.maximal_height = SPAWN_GUI_MAX_HEIGHT + spawn_opts_frame.auto_center = true + spawn_opts_frame.style.padding = 5 + + local inside_frame = spawn_opts_frame.add { + name = "spawn_opts_if", + type = "frame", + style = "inside_shallow_frame", + direction = "vertical" + } + + -- SUB HEADING w/ LABEL + local subhead = inside_frame.add{ + type="frame", + name="sub_header", + style = "changelog_subheader_frame" + } + subhead.style.height = 46 + AddLabel(subhead, "warning_lbl1", { "oarc-click-info-btn-help" }, my_note_style) + + return inside_frame +end --- Display the spawn options and explanation -function DisplaySpawnOptions(player) - if (player == nil) then - log("DisplaySpawnOptions with no valid player...") - return - end - - if (player.gui.screen.spawn_opts ~= nil) then - log("Tried to display spawn options when it was already displayed!") - return - end - player.gui.screen.add{name = "spawn_opts", - type = "frame", - direction = "vertical", - caption={"oarc-spawn-options"}} - local sGui = player.gui.screen.spawn_opts - sGui.style.maximal_width = SPAWN_GUI_MAX_WIDTH - sGui.style.maximal_height = SPAWN_GUI_MAX_HEIGHT - sGui.auto_center=true - - -- Warnings and explanations... - local warn_msg = {"oarc-click-info-btn-help"} - AddLabel(sGui, "warning_lbl1", warn_msg, my_warning_style) - AddLabel(sGui, "spawn_msg_lbl1", SPAWN_MSG1, my_label_style) - - -- Button and message about the regular vanilla spawn - -- if ENABLE_DEFAULT_SPAWN then - -- sGui.add{name = "default_spawn_btn", - -- type = "button", - -- caption={"oarc-vanilla-spawn"}} - -- local normal_spawn_text = {"oarc-default-spawn-behavior"} - -- AddLabel(sGui, "normal_spawn_lbl1", normal_spawn_text, my_label_style) - -- -- AddSpacerLine(sGui, "normal_spawn_spacer") - -- end - - -- The main spawning options. Solo near and solo far. - -- If enable, you can also choose to be on your own team. - local soloSpawnFlow = sGui.add{name = "spawn_solo_flow", - type = "frame", - direction="vertical", - style = "bordered_frame"} - - -- Radio buttons to pick your team. - if (global.ocfg.enable_separate_teams) then - soloSpawnFlow.add{name = "isolated_spawn_main_team_radio", - type = "radiobutton", - caption={"oarc-join-main-team-radio"}, - state=true} - soloSpawnFlow.add{name = "isolated_spawn_new_team_radio", - type = "radiobutton", - caption={"oarc-create-own-team-radio"}, - state=false} - end - - -- OPTIONS frame - -- AddLabel(soloSpawnFlow, "options_spawn_lbl1", - -- "Additional spawn options can be selected here. Not all are compatible with each other.", my_label_style) - - -- Allow players to spawn with a moat around their area. - if (global.ocfg.spawn_config.gen_settings.moat_choice_enabled and not global.ocfg.enable_vanilla_spawns) then - soloSpawnFlow.add{name = "isolated_spawn_moat_option_checkbox", - type = "checkbox", - caption={"oarc-moat-option"}, - state=false} - end - -- if (global.ocfg.enable_vanilla_spawns and (#global.vanillaSpawns > 0)) then - -- soloSpawnFlow.add{name = "isolated_spawn_vanilla_option_checkbox", - -- type = "checkbox", - -- caption="Use a pre-set vanilla spawn point. " .. #global.vanillaSpawns .. " available.", - -- state=false} - -- end - - -- Isolated spawn options. The core gameplay of this scenario. - local soloSpawnbuttons = soloSpawnFlow.add{name = "spawn_solo_flow", - type = "flow", - direction="horizontal"} - soloSpawnbuttons.style.horizontal_align = "center" - soloSpawnbuttons.style.horizontally_stretchable = true - soloSpawnbuttons.add{name = "isolated_spawn_near", - type = "button", - caption={"oarc-solo-spawn-near"}, - style = "confirm_button"} - soloSpawnbuttons.add{name = "isolated_spawn_far", - type = "button", - caption={"oarc-solo-spawn-far"}, - style = "confirm_button"} - - if (global.ocfg.enable_vanilla_spawns) then - AddLabel(soloSpawnFlow, "isolated_spawn_lbl1", - {"oarc-starting-area-vanilla"}, my_label_style) - AddLabel(soloSpawnFlow, "vanilla_spawn_lbl2", - {"oarc-vanilla-spawns-available", #global.vanillaSpawns}, my_label_style) - else - AddLabel(soloSpawnFlow, "isolated_spawn_lbl1", - {"oarc-starting-area-normal"}, my_label_style) - end - - -- Spawn options to join another player's base. - local sharedSpawnFrame = sGui.add{name = "spawn_shared_flow", - type = "frame", - direction="vertical", - style = "bordered_frame"} - if global.ocfg.enable_shared_spawns then - local numAvailSpawns = GetNumberOfAvailableSharedSpawns() - if (numAvailSpawns > 0) then - sharedSpawnFrame.add{name = "join_other_spawn", - type = "button", - caption={"oarc-join-someone-avail", numAvailSpawns}} - local join_spawn_text = {"oarc-join-someone-info"} - AddLabel(sharedSpawnFrame, "join_other_spawn_lbl1", join_spawn_text, my_label_style) - else - AddLabel(sharedSpawnFrame, "join_other_spawn_lbl1", {"oarc-no-shared-avail"}, my_label_style) - sharedSpawnFrame.add{name = "join_other_spawn_check", - type = "button", - caption={"oarc-join-check-again"}} - end - else - AddLabel(sharedSpawnFrame, "join_other_spawn_lbl1", - {"oarc-shared-spawn-disabled"}, my_warning_style) - end - - -- Awesome buddy spawning system - if (not global.ocfg.enable_vanilla_spawns) then - if global.ocfg.enable_shared_spawns and global.ocfg.enable_buddy_spawn then - local buddySpawnFrame = sGui.add{name = "spawn_buddy_flow", - type = "frame", - direction="vertical", - style = "bordered_frame"} - - -- AddSpacerLine(buddySpawnFrame, "buddy_spawn_msg_spacer") - buddySpawnFrame.add{name = "buddy_spawn", - type = "button", - caption={"oarc-buddy-spawn"}} - AddLabel(buddySpawnFrame, "buddy_spawn_lbl1", - {"oarc-buddy-spawn-info"} , my_label_style) +---Show the surface select dropdown +---@param parent_flow LuaGuiElement +---@return nil +function CreateSurfaceSelectDropdown(parent_flow) + local surfacesHorizontalFlow = parent_flow.add { + name = "surfaces_horizontal_flow", + type = "flow", + direction = "horizontal" + } + + local surface_list = GetAllowedSurfaces() + + -- Get the index of the default surface if it exists + local default_surface_index = 1 + for i,surface in ipairs(surface_list) do + if (surface == global.ocfg.gameplay.default_surface) then + default_surface_index = i + break end end - -- Some final notes - if (global.ocfg.max_players_shared_spawn > 0) then - AddLabel(sGui, "max_players_lbl2", - {"oarc-max-players-shared-spawn", global.ocfg.max_players_shared_spawn-1}, - my_note_style) - end - local spawn_distance_notes={"oarc-spawn-dist-notes", global.ocfg.near_dist_start, global.ocfg.near_dist_end, global.ocfg.far_dist_start, global.ocfg.far_dist_end} - AddLabel(sGui, "note_lbl1", spawn_distance_notes, my_note_style) + AddLabel(surfacesHorizontalFlow, "surfacesHorizontalFlowLabel", "Select Surface: ", my_label_style) + surfacesHorizontalFlow.add { + name = "surface_select_dropdown", + tags = { action = "oarc_spawn_options", setting = "surface_select" }, + type = "drop-down", + items = surface_list, + selected_index = default_surface_index, + tooltip = { "oarc-surface-select-tooltip" }, + enabled = #surface_list > 1 + } end - --- This just updates the radio buttons/checkboxes when players click them. -function SpawnOptsRadioSelect(event) - if not (event and event.element and event.element.valid) then return end - local elemName = event.element.name - - if (elemName == "isolated_spawn_main_team_radio") then - event.element.parent.isolated_spawn_new_team_radio.state=false - elseif (elemName == "isolated_spawn_new_team_radio") then - event.element.parent.isolated_spawn_main_team_radio.state=false +---Display the team select radio buttons +---@param parent_flow LuaGuiElement +---@param enable_main_team boolean +---@param enable_separate_teams boolean +---@return nil +function DisplayTeamSelectRadioButtons(parent_flow, enable_main_team, enable_separate_teams) + if enable_main_team then + parent_flow.add { + name = "isolated_spawn_main_team_radio", + tags = { action = "oarc_spawn_options", setting = "team_select", value = SPAWN_TEAM_CHOICE.join_main_team }, + type = "radiobutton", + caption = { "oarc-join-main-team-radio" }, + tooltip = { "oarc-join-main-team-tooltip" }, + -- If separate teams are not enabled, default to joining the main team, and disable the radio buttons. + state = true, + -- ignored_by_interaction = not enable_separate_teams, + -- enabled = enable_separate_teams + } end - if (elemName == "buddy_spawn_main_team_radio") then - event.element.parent.buddy_spawn_new_team_radio.state=false - event.element.parent.buddy_spawn_buddy_team_radio.state=false - elseif (elemName == "buddy_spawn_new_team_radio") then - event.element.parent.buddy_spawn_main_team_radio.state=false - event.element.parent.buddy_spawn_buddy_team_radio.state=false - elseif (elemName == "buddy_spawn_buddy_team_radio") then - event.element.parent.buddy_spawn_main_team_radio.state=false - event.element.parent.buddy_spawn_new_team_radio.state=false + if (enable_separate_teams) then + parent_flow.add { + name = "isolated_spawn_new_team_radio", + tags = { action = "oarc_spawn_options", setting = "team_select", value = SPAWN_TEAM_CHOICE.join_own_team }, + type = "radiobutton", + caption = { "oarc-create-own-team-radio" }, + tooltip = { "oarc-create-own-team-tooltip" }, + -- If main team is not enabled, default to joining the a separate team, and disable the radio buttons. + state = not enable_main_team, + -- ignored_by_interaction = not enable_main_team, + -- enabled = enable_main_team + } end end +---Create a distance select slider +---@param parent_flow LuaGuiElement +---@param minimum_distance number +---@param maximum_distance number +---@return nil +function CreateDistanceSelectSlider(parent_flow, minimum_distance, maximum_distance) + + local slider_flow = parent_flow.add { + type = "flow", + direction = "horizontal", + style = "player_input_horizontal_flow" + } + slider_flow.style.horizontally_stretchable = true + + local label = slider_flow.add { + type = "label", + caption = { "oarc-spawn-distance-slider-label" }, + tooltip = { "oarc-spawn-distance-slider-tooltip" } + } + label.style.horizontal_align = "left" + local slider = slider_flow.add { + name = "spawn_distance_slider", + type = "slider", + tags = { action = "oarc_spawn_options", setting = "distance_select" }, + minimum_value = minimum_distance, + maximum_value = maximum_distance, + value = minimum_distance, + discrete_slider = true, + value_step = 1, + tooltip = { "oarc-spawn-distance-slider-tooltip" } + } + slider.style.horizontally_stretchable = true + local text_value = slider_flow.add { + name = "spawn_distance_slider_value", + type = "textfield", + ignored_by_interaction = true, + caption = minimum_distance, + style = "slider_value_textfield", + text = tostring(minimum_distance) + } + text_value.style.horizontal_align = "right" + text_value.style.width = 50 +end --- Handle the gui click of the spawn options -function SpawnOptsGuiClick(event) - if not (event and event.element and event.element.valid) then return end - local player = game.players[event.player_index] - local elemName = event.element.name +---Create the spawn settings frame +---@param parent_flow LuaGuiElement +---@param gameplay OarcConfigGameplaySettings +---@return nil +function CreateSpawnSettingsFrame(parent_flow, gameplay) + + local spawn_settings_frame = parent_flow.add { + name = "spawn_settings_frame", + type = "frame", + direction = "vertical", + style = "bordered_frame", + } + spawn_settings_frame.style.horizontally_stretchable = true + spawn_settings_frame.style.padding = 5 + spawn_settings_frame.style.margin = 4 + spawn_settings_frame.style.bottom_margin = 0 + + AddLabel(spawn_settings_frame, nil, { "oarc-spawn-menu-settings-header" }, my_label_header_style) + AddLabel(spawn_settings_frame, nil, { "oarc-spawn-menu-settings-info" }, my_label_style) + + -- Pick surface + CreateSurfaceSelectDropdown(spawn_settings_frame) - if not player then - log("Another gui click happened with no valid player...") - return - end + -- Radio buttons to pick your team. + DisplayTeamSelectRadioButtons(spawn_settings_frame, gameplay.enable_main_team, gameplay.enable_separate_teams) - if (player.gui.screen.spawn_opts == nil) then - return -- Gui event unrelated to this gui. + -- Allow players to spawn with a moat around their area. + if (gameplay.allow_moats_around_spawns) then + spawn_settings_frame.add { + name = "isolated_spawn_moat_option_checkbox", + tags = { action = "oarc_spawn_options", setting = "moat_option" }, + type = "checkbox", + caption = { "oarc-moat-option" }, + state = false, + tooltip = { "oarc-moat-option-tooltip" } + } end - local pgcs = player.gui.screen.spawn_opts + CreateDistanceSelectSlider(spawn_settings_frame, gameplay.near_spawn_distance, gameplay.far_spawn_distance) +end - local joinMainTeamRadio, joinOwnTeamRadio, moatChoice, vanillaChoice = false +---Create a frame and a confim button for player to request SOLO spawn creation +---@param parent_flow LuaGuiElement +---@param enable_shared_spawns boolean +---@param max_shared_players integer +---@return nil +function CreateSoloSpawnFrame(parent_flow, enable_shared_spawns, max_shared_players) + + solo_spawn_frame = parent_flow.add { + name = "solo_spawn_frame", + type = "frame", + direction = "vertical", + style = "bordered_frame" + } + solo_spawn_frame.style.horizontally_stretchable = true + solo_spawn_frame.style.padding = 5 + solo_spawn_frame.style.margin = 4 + solo_spawn_frame.style.bottom_margin = 0 + + AddLabel(solo_spawn_frame, nil, { "oarc-spawn-menu-solo-header" }, my_label_header_style) + AddLabel(solo_spawn_frame, nil, { "oarc-starting-area-normal" }, my_label_style) + + -- A note about sharing spawns + if enable_shared_spawns and (max_shared_players > 1) then + AddLabel(solo_spawn_frame, nil, { "oarc-max-players-shared-spawn", max_shared_players - 1 }, my_label_style) + end - -- Check if a valid button on the gui was pressed - -- and delete the GUI - if ((elemName == "default_spawn_btn") or - (elemName == "isolated_spawn_near") or - (elemName == "isolated_spawn_far") or - (elemName == "join_other_spawn") or - (elemName == "buddy_spawn") or - (elemName == "join_other_spawn_check")) then + local button_flow = solo_spawn_frame.add { + type = "flow", + direction = "horizontal" + } + button_flow.style.horizontal_align = "right" + button_flow.style.horizontally_stretchable = true + button_flow.add { + name = "spawn_request", + tags = { action = "oarc_spawn_options", setting = "spawn_request" }, + type = "button", + caption = { "oarc-solo-spawn" }, + tooltip = { "oarc-solo-spawn-tooltip" }, + style = "green_button" + } +end - if (global.ocfg.enable_separate_teams) then - joinMainTeamRadio = - pgcs.spawn_solo_flow.isolated_spawn_main_team_radio.state - joinOwnTeamRadio = - pgcs.spawn_solo_flow.isolated_spawn_new_team_radio.state - else - joinMainTeamRadio = true - joinOwnTeamRadio = false +---Creates the shared spawn frame for joining another player's base +---@param parent_flow LuaGuiElement +---@param enable_shared_spawns boolean +---@return nil +function CreateSharedSpawnFrame(parent_flow, enable_shared_spawns) + + local shared_spawn_frame = parent_flow.shared_spawn_frame + local prev_selected_host = nil ---@type string? + local prev_selected_spawn = nil ---@type OarcUniqueSpawn? + + -- Create the shared spawn frame if it doesn't exist + if shared_spawn_frame == nil then + shared_spawn_frame = parent_flow.add { + name = "shared_spawn_frame", + type = "frame", + direction = "vertical", + style = "bordered_frame" + } + shared_spawn_frame.style.horizontally_stretchable = true + shared_spawn_frame.style.padding = 5 + shared_spawn_frame.style.margin = 4 + shared_spawn_frame.style.bottom_margin = 0 + + --- Let's us refresh the frame if it already exists instead of recreating it + else + -- Save the previous selected host so we can reselect it after the frame is recreated + if (shared_spawn_frame.shared_spawn_horizontal_flow ~= nil) then + local dropdown = shared_spawn_frame.shared_spawn_horizontal_flow.shared_spawn_select_dropdown + local index = dropdown.selected_index + if index > 0 then + prev_selected_host = dropdown.get_item(index) --[[@as string]] + end end - if (global.ocfg.spawn_config.gen_settings.moat_choice_enabled and not global.ocfg.enable_vanilla_spawns and - (pgcs.spawn_solo_flow.isolated_spawn_moat_option_checkbox ~= nil)) then - moatChoice = pgcs.spawn_solo_flow.isolated_spawn_moat_option_checkbox.state + + + for _,child in pairs(shared_spawn_frame.children) do + child.destroy() end - -- if (global.ocfg.enable_vanilla_spawns and - -- (pgcs.spawn_solo_flow.isolated_spawn_vanilla_option_checkbox ~= nil)) then - -- vanillaChoice = pgcs.spawn_solo_flow.isolated_spawn_vanilla_option_checkbox.state - -- end - pgcs.destroy() - else - return -- Do nothing, no valid element item was clicked. end - if (elemName == "default_spawn_btn") then - GivePlayerStarterItems(player) - ChangePlayerSpawn(player, player.force.get_spawn_position(GAME_SURFACE_NAME)) - SendBroadcastMsg({"oarc-player-is-joining-main-force", player.name}) - ChartArea(player.force, player.position, math.ceil(global.ocfg.spawn_config.gen_settings.land_area_tiles/CHUNK_SIZE), player.surface) - -- Unlock spawn control gui tab - SetOarcGuiTabEnabled(player, OARC_SPAWN_CTRL_GUI_NAME, true) + AddLabel(shared_spawn_frame, nil, { "oarc-spawn-menu-shared-header" }, my_label_header_style) - elseif ((elemName == "isolated_spawn_near") or (elemName == "isolated_spawn_far")) then + if not enable_shared_spawns then + AddLabel(shared_spawn_frame, nil, { "oarc-shared-spawn-disabled" }, my_warning_style) + return + end - -- Create a new spawn point - local newSpawn = {x=0,y=0} - -- Create a new force for player if they choose that radio button - if global.ocfg.enable_separate_teams and joinOwnTeamRadio then - local newForce = CreatePlayerCustomForce(player) - end + local avail_spawns = GetAvailableSharedSpawns() + local num_avail_spawns = #avail_spawns.hosts - -- Find an unused vanilla spawn - -- if (vanillaChoice) then - if (global.ocfg.enable_vanilla_spawns) then - if (elemName == "isolated_spawn_far") then - newSpawn = FindUnusedVanillaSpawn(game.surfaces[GAME_SURFACE_NAME], - global.ocfg.far_dist_end*CHUNK_SIZE) - elseif (elemName == "isolated_spawn_near") then - newSpawn = FindUnusedVanillaSpawn(game.surfaces[GAME_SURFACE_NAME], - global.ocfg.near_dist_start*CHUNK_SIZE) - end + if (num_avail_spawns > 0) then - -- Default OARC-type pre-set layout spawn. - else - -- Find coordinates of a good place to spawn - if (elemName == "isolated_spawn_far") then - newSpawn = FindUngeneratedCoordinates(global.ocfg.far_dist_start,global.ocfg.far_dist_end, player.surface) - elseif (elemName == "isolated_spawn_near") then - newSpawn = FindUngeneratedCoordinates(global.ocfg.near_dist_start,global.ocfg.near_dist_end, player.surface) - end - end + AddLabel(shared_spawn_frame, nil, { "oarc-join-someone-info" }, my_label_style) - -- If that fails, find a random map edge in a rand direction. - if ((newSpawn.x == 0) and (newSpawn.y == 0)) then - newSpawn = FindMapEdge(GetRandomVector(), player.surface) - log("Resorting to find map edge! x=" .. newSpawn.x .. ",y=" .. newSpawn.y) + local new_selected_index = 0 + if prev_selected_host then + for i,host in ipairs(avail_spawns.hosts) do + if host == prev_selected_host then + new_selected_index = i + break + end + end end - -- Create that player's spawn in the global vars - ChangePlayerSpawn(player, newSpawn) - - -- Send the player there - QueuePlayerForDelayedSpawn(player.name, newSpawn, moatChoice, global.ocfg.enable_vanilla_spawns) - if (elemName == "isolated_spawn_near") then - SendBroadcastMsg({"oarc-player-is-joining-near", player.name}) - elseif (elemName == "isolated_spawn_far") then - SendBroadcastMsg({"oarc-player-is-joining-far", player.name}) + local horizontal_flow = shared_spawn_frame.add { + name = "shared_spawn_horizontal_flow", + type = "flow", + direction = "horizontal", + style = "dialog_buttons_horizontal_flow" + } + horizontal_flow.style.horizontally_stretchable = true + + local label = AddLabel(horizontal_flow, nil, { "oarc-join-someone-dropdown-label" }, my_label_style) + label.style.horizontal_align = "left" + + local dropdown = horizontal_flow.add { + name = "shared_spawn_select_dropdown", + tags = { action = "oarc_spawn_options", setting = "shared_spawn_select" }, + type = "drop-down", + items = avail_spawns.hosts, + selected_index = new_selected_index, + tooltip = { "oarc-join-someone-dropdown-tooltip" } + } + dropdown.style.horizontal_align = "left" + + local dragger = horizontal_flow.add { + type = "empty-widget", + style = "draggable_space", + } + dragger.style.horizontally_stretchable = true + + local button = horizontal_flow.add { + name = "join_other_spawn", + tags = { action = "oarc_spawn_options", setting = "join_other_spawn" }, + type = "button", + tooltip = { "oarc-join-shared-button-tooltip" }, + enabled = new_selected_index > 0 + } + if new_selected_index == 0 then + button.caption = { "oarc-join-shared-button-disable" } + button.style = "red_button" + else + button.caption = { "oarc-join-shared-button-enable", prev_selected_host, avail_spawns.spawns[new_selected_index].surface_name } + button.style = "green_button" end - -- Unlock spawn control gui tab - SetOarcGuiTabEnabled(player, OARC_SPAWN_CTRL_GUI_NAME, true) - - player.print({"oarc-please-wait"}) - player.print({"", {"oarc-please-wait"}, "!"}) - player.print({"", {"oarc-please-wait"}, "!!"}) - - elseif (elemName == "join_other_spawn") then - DisplaySharedSpawnOptions(player) + button.style.horizontal_align = "right" - -- Provide a way to refresh the gui to check if people have shared their - -- bases. - elseif (elemName == "join_other_spawn_check") then - DisplaySpawnOptions(player) - - -- Hacky buddy spawn system - elseif (elemName == "buddy_spawn") then - table.insert(global.ocore.waitingBuddies, player.name) - SendBroadcastMsg({"oarc-looking-for-buddy", player.name}) - - DisplayBuddySpawnOptions(player) + else + AddLabel(shared_spawn_frame, nil, { "oarc-no-shared-avail" }, my_label_style) end end +---Refresh the shared spawn frame if it exists +---@param player LuaPlayer +---@return nil +function RefreshSharedSpawnFrameIfExist(player) + local spawn_opts = player.gui.screen.spawn_opts + if spawn_opts == nil then return end + CreateSharedSpawnFrame(spawn_opts.spawn_opts_if, global.ocfg.gameplay.enable_shared_spawns) +end --- Display the spawn options and explanation -function DisplaySharedSpawnOptions(player) - player.gui.screen.add{name = "shared_spawn_opts", - type = "frame", - direction = "vertical", - caption={"oarc-avail-bases-join"}} - - local shGuiFrame = player.gui.screen.shared_spawn_opts - shGuiFrame.auto_center = true - local shGui = shGuiFrame.add{type="scroll-pane", name="spawns_scroll_pane", caption=""} - ApplyStyle(shGui, my_fixed_width_style) - shGui.style.maximal_width = SPAWN_GUI_MAX_WIDTH - shGui.style.maximal_height = SPAWN_GUI_MAX_HEIGHT - shGui.horizontal_scroll_policy = "never" - - - for spawnName,sharedSpawn in pairs(global.ocore.sharedSpawns) do - if (sharedSpawn.openAccess and - (game.players[spawnName] ~= nil) and - game.players[spawnName].connected) then - local spotsRemaining = global.ocfg.max_players_shared_spawn - #global.ocore.sharedSpawns[spawnName].players - if (global.ocfg.max_players_shared_spawn == 0) then - shGui.add{type="button", caption=spawnName, name=spawnName} - elseif (spotsRemaining > 0) then - shGui.add{type="button", caption={"oarc-spawn-spots-remaining", spawnName, spotsRemaining}, name=spawnName} - end - if (shGui.spawnName ~= nil) then - -- AddSpacer(buddyGui, spawnName .. "spacer_lbl") - ApplyStyle(shGui[spawnName], my_small_button_style) +---Creates the buddy spawn frame for spawning with a buddy +---@param parent_flow LuaGuiElement +---@param player LuaPlayer +---@param enable_buddy_spawn boolean +---@param enable_separate_teams boolean +---@return nil +function CreateBuddySpawnFrame(parent_flow, player, enable_buddy_spawn, enable_separate_teams) + + local buddy_spawn_frame = parent_flow.buddy_spawn_frame + local selected_buddy = nil ---@type string? + + -- Create the buddy spawn frame if it doesn't exist + if buddy_spawn_frame == nil then + buddy_spawn_frame = parent_flow.add { + name = "buddy_spawn_frame", + type = "frame", + direction = "vertical", + style = "bordered_frame" + } + buddy_spawn_frame.style.horizontally_stretchable = true + buddy_spawn_frame.style.padding = 5 + buddy_spawn_frame.style.margin = 4 + + --- Let's us refresh the frame if it already exists instead of recreating it + else + -- Save the previous selected buddy so we can reselect it after the frame is recreated + if buddy_spawn_frame.waiting_buddies_dropdown ~= nil then + local index = buddy_spawn_frame.waiting_buddies_dropdown.selected_index + if index > 0 then + selected_buddy = buddy_spawn_frame.waiting_buddies_dropdown.get_item(index) --[[@as string]] + --- Make sure the buddy is still valid? + if game.players[selected_buddy] and game.players[selected_buddy].gui.screen.spawn_opts == nil then + selected_buddy = nil + end end end - end + for _,child in pairs(buddy_spawn_frame.children) do + child.destroy() + end + end - shGui.add{name = "shared_spawn_cancel", - type = "button", - caption={"oarc-cancel-return-to-previous"}, - style = "back_button"} -end + log("Creating buddy spawn frame for: " .. player.name) --- Handle the gui click of the shared spawn options -function SharedSpwnOptsGuiClick(event) - if not (event and event.element and event.element.valid) then return end - local player = game.players[event.player_index] - local buttonClicked = event.element.name + AddLabel(buddy_spawn_frame, nil, { "oarc-spawn-menu-buddy-header" }, my_label_header_style) - if not player then - log("Another gui click happened with no valid player...") + if not enable_buddy_spawn then + AddLabel(buddy_spawn_frame, nil, { "oarc-buddy-spawn-disabled" }, my_warning_style) return end - if (event.element.parent) then - if (event.element.parent.name ~= "spawns_scroll_pane") then - return - end + -- Warnings and explanations... + AddLabel(buddy_spawn_frame, nil, { "oarc-buddy-spawn-instructions" }, my_label_style) + + if (enable_separate_teams) then + buddy_spawn_frame.add { + tags = { action = "oarc_spawn_options", setting = "buddy_team_select" }, + type = "checkbox", + caption = { "oarc-create-buddy-team" }, + state = false, + tooltip = { "oarc-create-buddy-team-tooltip" } + } end - -- Check for cancel button, return to spawn options - if (buttonClicked == "shared_spawn_cancel") then - DisplaySpawnOptions(player) - if (player.gui.screen.shared_spawn_opts ~= nil) then - player.gui.screen.shared_spawn_opts.destroy() - end - - -- Else check for which spawn was selected - -- If a spawn is removed during this time, the button will not do anything - else - for spawnName,sharedSpawn in pairs(global.ocore.sharedSpawns) do - if ((buttonClicked == spawnName) and - (game.players[spawnName] ~= nil) and - (game.players[spawnName].connected)) then - - -- Add the player to that shared spawns join queue. - if (global.ocore.sharedSpawns[spawnName].joinQueue == nil) then - global.ocore.sharedSpawns[spawnName].joinQueue = {} - end - table.insert(global.ocore.sharedSpawns[spawnName].joinQueue, player.name) + ---@type string[] + local avail_buddies = GetOtherPlayersInSpawnMenu(player) - -- Clear the shared spawn options gui. - if (player.gui.screen.shared_spawn_opts ~= nil) then - player.gui.screen.shared_spawn_opts.destroy() - end + log("Available buddies: " .. serpent.block(avail_buddies)) - -- Display wait menu with cancel button. - DisplaySharedSpawnJoinWaitMenu(player) - - -- Tell other player they are requesting a response. - game.players[spawnName].print({"oarc-player-requesting-join-you", player.name}) + local previous_index = 0 + if selected_buddy then + for i,host in ipairs(avail_buddies) do + if host == selected_buddy then + previous_index = i break end end end -end -function DisplaySharedSpawnJoinWaitMenu(player) - - local sGui = player.gui.screen.add{name = "join_shared_spawn_wait_menu", - type = "frame", - direction = "vertical", - caption={"oarc-waiting-for-spawn-owner"}} - sGui.auto_center = true - sGui.style.maximal_width = SPAWN_GUI_MAX_WIDTH - sGui.style.maximal_height = SPAWN_GUI_MAX_HEIGHT - - - -- Warnings and explanations... - AddLabel(sGui, "warning_lbl1", {"oarc-you-will-spawn-once-host"}, my_warning_style) - sGui.add{name = "cancel_shared_spawn_wait_menu", - type = "button", - caption={"oarc-cancel-return-to-previous"}, - style = "back_button"} + local buddy_button_horizontal_flow = buddy_spawn_frame.add { + type = "flow", + direction = "horizontal", + style = "dialog_buttons_horizontal_flow" + } + buddy_button_horizontal_flow.style.horizontally_stretchable = true + + local label = AddLabel(buddy_button_horizontal_flow, nil, { "oarc-buddy-select-label" }, my_label_style) + label.style.horizontal_align = "left" + + local buddy_dropdown = buddy_button_horizontal_flow.add { + name = "waiting_buddies_dropdown", + tags = { action = "oarc_spawn_options", setting = "buddy_select" }, + type = "drop-down", + items = avail_buddies, + selected_index = previous_index, + tooltip = { "oarc-buddy-select-tooltip" } + } + buddy_dropdown.style.horizontal_align = "left" + + local empty = buddy_button_horizontal_flow.add { + type = "empty-widget", + style = "draggable_space", + } + empty.style.horizontally_stretchable = true + + local button = buddy_button_horizontal_flow.add { + name = "buddy_spawn_request", + tags = { action = "oarc_spawn_options", setting = "buddy_spawn_request" }, + type = "button", + caption = { "oarc-buddy-spawn" }, + style = "green_button", + tooltip = { "oarc-buddy-spawn-tooltip" } + } + button.style.horizontal_align = "right" end --- Handle the gui click of the buddy wait menu -function SharedSpawnJoinWaitMenuClick(event) - if not (event and event.element and event.element.valid) then return end - local player = game.players[event.player_index] - local elemName = event.element.name +---Refresh the buddy list without recreating any GUI elements +---@param player LuaPlayer +---@param dropdown LuaGuiElement The buddy dropdown element +---@return nil +function RefreshBuddyList(player, dropdown) + log("Refreshing buddy list for: " .. player.name) + dropdown.items = GetOtherPlayersInSpawnMenu(player) +end - if not player then - log("Another gui click happened with no valid player...") +---Display the spawn options and explanation +---@param player LuaPlayer +---@return nil +function DisplaySpawnOptions(player) + if (player == nil) then + log("DisplaySpawnOptions with no valid player...") return end - if (player.gui.screen.join_shared_spawn_wait_menu == nil) then - return -- Gui event unrelated to this gui. + if (player.gui.screen.spawn_opts ~= nil) then + log("Tried to display spawn options when it was already displayed!") + return end - -- Check if player is cancelling the request. - if (elemName == "cancel_shared_spawn_wait_menu") then - player.gui.screen.join_shared_spawn_wait_menu.destroy() - DisplaySpawnOptions(player) + -- Get gameplay settings from config + ---@type OarcConfigGameplaySettings + local gameplay = global.ocfg.gameplay - -- Find and remove the player from the joinQueue they were in. - for spawnName,sharedSpawn in pairs(global.ocore.sharedSpawns) do - if (sharedSpawn.joinQueue ~= nil) then - for index,requestingPlayer in pairs(sharedSpawn.joinQueue) do - if (requestingPlayer == player.name) then - global.ocore.sharedSpawns[spawnName].joinQueue[index] = false - game.players[spawnName].print({"oarc-player-cancel-join-request", player.name}) - return - end - end - end - end + -- Create the primary frame and a warning label + local sGui = CreateSpawnMenuGuiFrame(player) - log("ERROR! Failed to remove player from joinQueue!") + -- Create the default settings entry for the OarcSpawnChoices table + local default_team = SPAWN_TEAM_CHOICE.join_main_team + if (not gameplay.enable_main_team and gameplay.enable_separate_teams) then + default_team = SPAWN_TEAM_CHOICE.join_own_team end + ---@type OarcSpawnChoices + local spawn_choices_entry = { + surface = global.ocfg.gameplay.default_surface, + team = default_team, + moat = false, + buddy = nil, + distance = global.ocfg.gameplay.near_spawn_distance, + host = nil, + buddy_team = false + } + global.spawn_choices[player.name] = spawn_choices_entry + + CreateSpawnSettingsFrame(sGui, gameplay) -- The settings for configuring a spawn + CreateSoloSpawnFrame(sGui, gameplay.enable_shared_spawns, gameplay.number_of_players_per_shared_spawn) -- The primary method of spawning + CreateSharedSpawnFrame(sGui, gameplay.enable_shared_spawns) -- Spawn options to join another player's base. + CreateBuddySpawnFrame(sGui, player, gameplay.enable_buddy_spawn, gameplay.enable_separate_teams) -- Awesome buddy spawning system end -local function IsSharedSpawnActive(player) - if ((global.ocore.sharedSpawns[player.name] == nil) or - (global.ocore.sharedSpawns[player.name].openAccess == false)) then - return false - else - return true - end -end +---This just updates the radio buttons/checkboxes when players click them. +---@param event EventData.on_gui_checked_state_changed +---@return nil +function SpawnOptsRadioSelect(event) + if not event.element.valid then return end + local elemName = event.element.name + local player = game.players[event.player_index] + local tags = event.element.tags --- Get a random warp point to go to -function GetRandomSpawnPoint() - local numSpawnPoints = TableLength(global.ocore.sharedSpawns) - if (numSpawnPoints > 0) then - local randSpawnNum = math.random(1,numSpawnPoints) - local counter = 1 - for _,sharedSpawn in pairs(global.ocore.sharedSpawns) do - if (randSpawnNum == counter) then - return sharedSpawn.position - end - counter = counter + 1 - end + if (tags.action ~= "oarc_spawn_options") then + return end - return {x=0,y=0} -end + if (tags.setting == "team_select") then + global.spawn_choices[player.name].team = tags.value --[[@as SpawnTeamChoice]] --- This is a toggle function, it either shows or hides the spawn controls -function CreateSpawnCtrlGuiTab(tab_container, player) - local spwnCtrls = tab_container.add{ - type="scroll-pane", - name="spwn_ctrl_panel", - caption=""} - ApplyStyle(spwnCtrls, my_fixed_width_style) - spwnCtrls.style.maximal_height = SPAWN_GUI_MAX_HEIGHT - spwnCtrls.horizontal_scroll_policy = "never" - - if global.ocfg.enable_shared_spawns then - if (global.ocore.uniqueSpawns[player.name] ~= nil) then - -- This checkbox allows people to join your base when they first - -- start the game. - spwnCtrls.add{type="checkbox", name="accessToggle", - caption={"oarc-spawn-allow-joiners"}, - state=IsSharedSpawnActive(player)} - ApplyStyle(spwnCtrls["accessToggle"], my_fixed_width_style) + -- Need to handle the radio button logic manually + if (elemName == "isolated_spawn_main_team_radio") then + if (event.element.parent.isolated_spawn_new_team_radio ~= nil) then + event.element.parent.isolated_spawn_new_team_radio.state = false + end + elseif (elemName == "isolated_spawn_new_team_radio") then + event.element.parent.isolated_spawn_main_team_radio.state = false end - end - - -- @todo Figure out why this case could be hit... Fix for error report in github. - if (global.ocore.playerCooldowns[player.name] == nil) then - global.ocore.playerCooldowns[player.name] = {setRespawn=game.tick} - end - -- Sets the player's custom spawn point to their current location - if ((game.tick - global.ocore.playerCooldowns[player.name].setRespawn) > - (global.ocfg.respawn_cooldown_min * TICKS_PER_MINUTE)) then - spwnCtrls.add{type="button", name="setRespawnLocation", caption={"oarc-set-respawn-loc"}} - spwnCtrls["setRespawnLocation"].style.font = "default-small-semibold" + elseif (tags.setting == "buddy_team_select") then + global.spawn_choices[player.name].buddy_team = event.element.state - else - AddLabel(spwnCtrls,"respawn_cooldown_note1", - {"oarc-set-respawn-loc-cooldown", formattime((global.ocfg.respawn_cooldown_min * TICKS_PER_MINUTE)-(game.tick - global.ocore.playerCooldowns[player.name].setRespawn))}, my_note_style) - end - AddLabel(spwnCtrls, "respawn_cooldown_note2", {"oarc-set-respawn-note"}, my_note_style) - - -- Display a list of people in the join queue for your base. - if (global.ocfg.enable_shared_spawns and IsSharedSpawnActive(player)) then - if ((global.ocore.sharedSpawns[player.name].joinQueue ~= nil) and - (#global.ocore.sharedSpawns[player.name].joinQueue > 0)) then - - - AddLabel(spwnCtrls, "drop_down_msg_lbl1", {"oarc-select-player-join-queue"}, my_label_style) - spwnCtrls.add{name = "join_queue_dropdown", - type = "drop-down", - items = global.ocore.sharedSpawns[player.name].joinQueue} - spwnCtrls.add{name = "accept_player_request", - type = "button", - caption={"oarc-accept"}} - spwnCtrls.add{name = "reject_player_request", - type = "button", - caption={"oarc-reject"}} - else - AddLabel(spwnCtrls, "empty_join_queue_note1", {"oarc-no-player-join-reqs"}, my_note_style) - end - spwnCtrls.add{name = "join_queue_spacer", type = "label", - caption=" "} + elseif (tags.setting == "moat_option") then + global.spawn_choices[player.name].moat = event.element.state end end -function SpawnCtrlGuiOptionsSelect(event) - if not (event and event.element and event.element.valid) then return end - +---Handle the gui click of the spawn options +---@param event EventData.on_gui_click +---@return nil +function SpawnOptsGuiClick(event) + if not event.element.valid then return end local player = game.players[event.player_index] - local name = event.element.name + local tags = event.element.tags - if not player then - log("Another gui click happened with no valid player...") + if (tags.action ~= "oarc_spawn_options") then return end - -- Handle changes to spawn sharing. - if (name == "accessToggle") then - if event.element.state then - if DoesPlayerHaveCustomSpawn(player) then - if (global.ocore.sharedSpawns[player.name] == nil) then - CreateNewSharedSpawn(player) - else - global.ocore.sharedSpawns[player.name].openAccess = true - end - - SendBroadcastMsg({"oarc-start-shared-base", player.name}) - end - else - if (global.ocore.sharedSpawns[player.name] ~= nil) then - global.ocore.sharedSpawns[player.name].openAccess = false - SendBroadcastMsg({"oarc-stop-shared-base", player.name}) - end + if (tags.setting == "welcome_okay") then + if (player.gui.screen.welcome_msg ~= nil) then + player.gui.screen.welcome_msg.destroy() end - FakeTabChangeEventOarcGui(player) + DisplaySpawnOptions(player) + elseif (tags.setting == "spawn_request") then + PrimarySpawnRequest(player) + elseif (tags.setting == "join_other_spawn") then + RequestToJoinSharedSpawn(player) + elseif (tags.setting == "cancel_shared_spawn_wait_menu") then + CancelSharedSpawnRequest(player) + elseif (tags.setting == "buddy_select") then + RefreshBuddyList(player, event.element) + elseif (tags.setting == "buddy_spawn_request") then + RequestBuddySpawn(player) + elseif (tags.setting == "cancel_buddy_wait_menu") then + CancelBuddySpawnWaitMenu(player) + elseif (tags.setting == "accept_buddy_request") then + AcceptBuddyRequest(player, tags.requesting_buddy_name --[[@as string]]) + elseif (tags.setting == "reject_buddy_request") then + RejectBuddyRequest(player, tags.requesting_buddy_name --[[@as string]]) + elseif (tags.setting == "surface_select") then + event.element.items = GetAllowedSurfaces() end end -function SpawnCtrlGuiClick(event) - if not (event and event.element and event.element.valid) then return end +---Request a buddy spawn. Requires the buddy to accept or reject the request. +---@param player LuaPlayer +---@return nil +function RequestBuddySpawn(player) + local buddy_choice = global.spawn_choices[player.name].buddy + if (buddy_choice == nil) then player.print({ "oarc-invalid-buddy" }) return end + local buddy = game.players[buddy_choice] + if (buddy == nil) then player.print({ "oarc-invalid-buddy" }) return end + -- Confirm the buddy is still in the spawn menu! + if (buddy.gui.screen.spawn_opts == nil) then player.print({ "oarc-invalid-buddy", buddy.name }) return end + + DisplayBuddySpawnWaitMenu(player) + DisplayBuddySpawnRequestMenu(buddy, player.name) +end - local player = game.players[event.player_index] - local elemName = event.element.name +---Handle buddy spawn wait menu cancel (waiting for buddy to accept or decline proposal) +---@param player LuaPlayer +---@return nil +function CancelBuddySpawnWaitMenu(player) - if not player then - log("Another gui click happened with no valid player...") - return - end + ---@type OarcSpawnChoices + local spawn_choices = global.spawn_choices[player.name] + local buddy = game.players[spawn_choices.buddy] - if (event.element.parent) then - if (event.element.parent.name ~= "spwn_ctrl_panel") then - return - end - end + player.gui.screen.buddy_wait_menu.destroy() + DisplaySpawnOptions(player) - -- Sets a new respawn point and resets the cooldown. - if (elemName == "setRespawnLocation") then - if DoesPlayerHaveCustomSpawn(player) then - ChangePlayerSpawn(player, player.position) - FakeTabChangeEventOarcGui(player) - player.print({"oarc-spawn-point-updated"}) - end + -- Catch a case where the buddy has left the game early and no longer exists. + if (buddy == nil) then + return end - -- Accept or reject pending player join requests to a shared base - if ((elemName == "accept_player_request") or (elemName == "reject_player_request")) then - - if ((event.element.parent.join_queue_dropdown == nil) or - (event.element.parent.join_queue_dropdown.selected_index == 0)) then - player.print({"oarc-selected-player-not-wait"}) - FakeTabChangeEventOarcGui(player) - return - end - - local joinQueueIndex = event.element.parent.join_queue_dropdown.selected_index - local joinQueuePlayerChoice = event.element.parent.join_queue_dropdown.get_item(joinQueueIndex) + if (buddy.gui.screen.buddy_request_menu ~= nil) then + buddy.gui.screen.buddy_request_menu.destroy() + DisplaySpawnOptions(buddy) + end - if ((game.players[joinQueuePlayerChoice] == nil) or - (not game.players[joinQueuePlayerChoice].connected)) then - player.print({"oarc-selected-player-not-wait"}) - FakeTabChangeEventOarcGui(player) - return - end + buddy.print({ "oarc-buddy-cancel-request", player.name }) +end - if (elemName == "reject_player_request") then - player.print({"oarc-reject-joiner", joinQueuePlayerChoice}) - SendMsg(joinQueuePlayerChoice, {"oarc-your-request-rejected"}) - FakeTabChangeEventOarcGui(player) +---Request to join someone's shared spawn +---@param player LuaPlayer +---@return nil +function RequestToJoinSharedSpawn(player) - -- Close the waiting players menu - if (game.players[joinQueuePlayerChoice].gui.screen.join_shared_spawn_wait_menu) then - game.players[joinQueuePlayerChoice].gui.screen.join_shared_spawn_wait_menu.destroy() - DisplaySpawnOptions(game.players[joinQueuePlayerChoice]) - end + if (player.gui.screen.spawn_opts ~= nil) then + player.gui.screen.spawn_opts.destroy() + end - -- Find and remove the player from the joinQueue they were in. - for index,requestingPlayer in pairs(global.ocore.sharedSpawns[player.name].joinQueue) do - if (requestingPlayer == joinQueuePlayerChoice) then - global.ocore.sharedSpawns[player.name].joinQueue[index] = nil - return - end - end + local host_name = global.spawn_choices[player.name].host + if (host_name == nil) then player.print({ "oarc-no-shared-spawn-selected" }) return end - elseif (elemName == "accept_player_request") then + -- Clear the spawn options gui + if (player.gui.screen.spawn_opts ~= nil) then + player.gui.screen.spawn_opts.destroy() + end - -- Find and remove the player from the joinQueue they were in. - for index,requestingPlayer in pairs(global.ocore.sharedSpawns[player.name].joinQueue) do - if (requestingPlayer == joinQueuePlayerChoice) then - global.ocore.sharedSpawns[player.name].joinQueue[index] = nil - end - end + if ((game.players[host_name] ~= nil) and (game.players[host_name].connected)) then + local primary_spawn = FindPrimaryUniqueSpawn(host_name) + table.insert(global.unique_spawns[primary_spawn.surface_name][host_name].join_queue, player.name) - -- If player exists, then do stuff. - if (game.players[joinQueuePlayerChoice]) then - -- Send an announcement - SendBroadcastMsg({"oarc-player-joining-base", joinQueuePlayerChoice, player.name}) + -- Display wait menu with cancel button. + DisplaySharedSpawnJoinWaitMenu(player) - -- Close the waiting players menu - if (game.players[joinQueuePlayerChoice].gui.screen.join_shared_spawn_wait_menu) then - game.players[joinQueuePlayerChoice].gui.screen.join_shared_spawn_wait_menu.destroy() - end + -- Tell other player they are requesting a response. + game.players[host_name].print({ "oarc-player-requesting-join-you", player.name }) + OarcGuiRefreshContent(game.players[host_name]) + else + player.print({ "oarc-invalid-host-shared-spawn" }) - -- Spawn the player - local joiningPlayer = game.players[joinQueuePlayerChoice] - ChangePlayerSpawn(joiningPlayer, global.ocore.sharedSpawns[player.name].position) - SendPlayerToSpawn(joiningPlayer) - GivePlayerStarterItems(joiningPlayer) - table.insert(global.ocore.sharedSpawns[player.name].players, joiningPlayer.name) - joiningPlayer.force = game.players[player.name].force - - -- Render some welcoming text... - DisplayWelcomeGroundTextAtSpawn(joiningPlayer, global.ocore.sharedSpawns[player.name].position) - - -- Unlock spawn control gui tab - SetOarcGuiTabEnabled(joiningPlayer, OARC_SPAWN_CTRL_GUI_NAME, true) - else - SendBroadcastMsg({"oarc-player-left-while-joining", joinQueuePlayerChoice}) - end - end + DisplaySpawnOptions(player) end end --- Display the buddy spawn menu -function DisplayBuddySpawnOptions(player) - local buddyGui = player.gui.screen.add{name = "buddy_spawn_opts", - type = "frame", - direction = "vertical", - caption={"oarc-buddy-spawn-options"}} - buddyGui.auto_center = true - buddyGui.style.maximal_width = SPAWN_GUI_MAX_WIDTH - buddyGui.style.maximal_height = SPAWN_GUI_MAX_HEIGHT +---Cancel shared spawn request from the waiting menu +---@param player LuaPlayer +---@return nil +function CancelSharedSpawnRequest(player) - -- Warnings and explanations... - AddLabel(buddyGui, "buddy_info_msg", {"oarc-buddy-spawn-instructions"}, my_label_style) - AddSpacer(buddyGui) - - -- The buddy spawning options. - local buddySpawnFlow = buddyGui.add{name = "spawn_buddy_flow", - type = "frame", - direction="vertical", - style = "bordered_frame"} - - buddyList = {} - for _,buddyName in pairs(global.ocore.waitingBuddies) do - if (buddyName ~= player.name) then - table.insert(buddyList, buddyName) - end + local host_name = global.spawn_choices[player.name].host + if (host_name ~= nil) and (game.players[host_name] ~= nil) then + game.players[host_name].print({ "oarc-player-cancel-join-request", player.name }) end - AddLabel(buddySpawnFlow, "drop_down_msg_lbl1", {"oarc-buddy-select-info"}, my_label_style) - buddySpawnFlow.add{name = "waiting_buddies_dropdown", - type = "drop-down", - items = buddyList} - buddySpawnFlow.add{name = "refresh_buddy_list", - type = "button", - caption={"oarc-buddy-refresh"}} - -- AddSpacerLine(buddySpawnFlow) - - -- Allow picking of teams - if (global.ocfg.enable_separate_teams) then - buddySpawnFlow.add{name = "buddy_spawn_main_team_radio", - type = "radiobutton", - caption={"oarc-join-main-team-radio"}, - state=true} - buddySpawnFlow.add{name = "buddy_spawn_new_team_radio", - type = "radiobutton", - caption={"oarc-create-own-team-radio"}, - state=false} - buddySpawnFlow.add{name = "buddy_spawn_buddy_team_radio", - type = "radiobutton", - caption={"oarc-create-buddy-team"}, - state=false} - end - if (global.ocfg.spawn_config.gen_settings.moat_choice_enabled) then - buddySpawnFlow.add{name = "buddy_spawn_moat_option_checkbox", - type = "checkbox", - caption={"oarc-moat-option"}, - state=false} - end - - -- AddSpacerLine(buddySpawnFlow) - buddySpawnFlow.add{name = "buddy_spawn_request_near", - type = "button", - caption={"oarc-buddy-spawn-near"}, - style = "confirm_button"} - buddySpawnFlow.add{name = "buddy_spawn_request_far", - type = "button", - caption={"oarc-buddy-spawn-far"}, - style = "confirm_button"} - - AddSpacer(buddyGui) - buddyGui.add{name = "buddy_spawn_cancel", - type = "button", - caption={"oarc-cancel-return-to-previous"}, - style = "back_button"} - - -- Some final notes - AddSpacerLine(buddyGui) - if (global.ocfg.max_players_shared_spawn > 0) then - AddLabel(buddyGui, "buddy_max_players_lbl1", - {"oarc-max-players-shared-spawn", global.ocfg.max_players_shared_spawn-1}, - my_note_style) - end - local spawn_distance_notes={"oarc-spawn-dist-notes", global.ocfg.near_dist_start, global.ocfg.near_dist_end, global.ocfg.far_dist_start, global.ocfg.far_dist_end} - AddLabel(buddyGui, "note_lbl1", spawn_distance_notes, my_note_style) -end + --- Destroy the waiting menu and display the spawn options again. + player.gui.screen.join_shared_spawn_wait_menu.destroy() + DisplaySpawnOptions(player) + RemovePlayerFromJoinQueue(player.name) + log("ERROR! Failed to remove player from joinQueue?!") +end --- Handle the gui click of the spawn options -function BuddySpawnOptsGuiClick(event) - if not (event and event.element and event.element.valid) then return end +---Handle slider value changes +---@param event EventData.on_gui_value_changed +---@return nil +function SpawnOptsValueChanged(event) + if not event.element.valid then return end local player = game.players[event.player_index] - local elemName = event.element.name + local tags = event.element.tags - if not player then - log("Another gui click happened with no valid player...") + if (tags.action ~= "oarc_spawn_options") then return end - if (player.gui.screen.buddy_spawn_opts == nil) then - return -- Gui event unrelated to this gui. + if (tags.setting == "distance_select") then + local distance = event.element.slider_value + global.spawn_choices[player.name].distance = distance + event.element.parent.spawn_distance_slider_value.text = tostring(distance) + -- log("GUI DEBUG Selected distance: " .. distance) end +end - local waiting_buddies_dropdown = player.gui.screen.buddy_spawn_opts.spawn_buddy_flow.waiting_buddies_dropdown - - -- Just refresh the buddy list dropdown values only. - if (elemName == "refresh_buddy_list") then - waiting_buddies_dropdown.clear_items() +---Handle dropdown selection changes +---@param event EventData.on_gui_selection_state_changed +---@return nil +function SpawnOptsSelectionChanged(event) + if not event.element.valid then return end + local player = game.players[event.player_index] + local tags = event.element.tags - for _,buddyName in pairs(global.ocore.waitingBuddies) do - if (player.name ~= buddyName) then - waiting_buddies_dropdown.add_item(buddyName) - end - end + if (tags.action ~= "oarc_spawn_options") then return end - -- Handle the cancel button to exit this menu - if (elemName == "buddy_spawn_cancel") then - player.gui.screen.buddy_spawn_opts.destroy() - DisplaySpawnOptions(player) + if (tags.setting == "surface_select") then + local index = event.element.selected_index + local surface_name = event.element.get_item(index) --[[@as string]] + global.spawn_choices[player.name].surface = surface_name + log("GUI DEBUG Selected surface: " .. surface_name) + + elseif (tags.setting == "shared_spawn_select") then + SharedSpawnSelect(event.element, player) + + elseif (tags.setting == "buddy_select") then + local index = event.element.selected_index + if (index > 0) then + local buddyName = event.element.get_item(index) --[[@as string]] + global.spawn_choices[player.name].buddy = buddyName + log("GUI DEBUG Selected buddy: " .. buddyName) + else + global.spawn_choices[player.name].buddy = nil + end + end +end - -- Remove them from the buddy list when they cancel - for i=#global.ocore.waitingBuddies,1,-1 do - if (global.ocore.waitingBuddies[i] == player.name) then - global.ocore.waitingBuddies[i] = nil - end +---Handles the shared spawn host dropdown selection. Updates the join button and sets the host spawn choice entry. +---@param gui_element LuaGuiElement +---@param player LuaPlayer +---@return nil +function SharedSpawnSelect(gui_element, player) + local index = gui_element.selected_index + if (index > 0) then + local host_name = gui_element.get_item(index) --[[@as string]] + local button = gui_element.parent.join_other_spawn + + local primary_spawn = FindPrimaryUniqueSpawn(host_name) + if (IsSharedSpawnOpen(primary_spawn.surface_name, host_name) and not IsSharedSpawnFull(primary_spawn.surface_name, host_name)) then + global.spawn_choices[player.name].host = host_name + button.enabled = true + button.caption = { "oarc-join-shared-button-enable", host_name, primary_spawn.surface_name } + button.style = "green_button" + else + player.print({ "oarc-invalid-host-shared-spawn" }) + global.spawn_choices[player.name].host = nil + gui_element.selected_index = 0 + button.enabled = false + button.caption = { "oarc-join-shared-button-disable" } + button.style = "red_button" end + + else + global.spawn_choices[player.name].host = nil end +end - local joinMainTeamRadio, joinOwnTeamRadio, joinBuddyTeamRadio, moatChoice = false - local buddyChoice = nil +---Requests the generation of a spawn point for the player (their first primary spawn) +---@param player LuaPlayer +---@return nil +function PrimarySpawnRequest(player) + -- Get the player's spawn choices + ---@type OarcSpawnChoices + local spawn_choices = global.spawn_choices[player.name] + if (spawn_choices == nil) then error("ERROR! No spawn choices found for player!") return end - -- Handle the spawn request button clicks - if ((elemName == "buddy_spawn_request_near") or - (elemName == "buddy_spawn_request_far")) then + -- N/A for solo spawns so clear these! + global.spawn_choices[player.name].host = nil + global.spawn_choices[player.name].buddy = nil - local buddySpawnGui = player.gui.screen.buddy_spawn_opts.spawn_buddy_flow + -- Cache some useful variables + local surface = game.surfaces[spawn_choices.surface] - local dropDownIndex = buddySpawnGui.waiting_buddies_dropdown.selected_index - if ((dropDownIndex > 0) and (dropDownIndex <= #buddySpawnGui.waiting_buddies_dropdown.items)) then - buddyChoice = buddySpawnGui.waiting_buddies_dropdown.get_item(dropDownIndex) - else - player.print({"oarc-invalid-buddy"}) - return - end + -- Find coordinates of a good place to spawn + local spawn_position = FindUngeneratedCoordinates(surface, spawn_choices.distance, 3) - local buddyIsStillWaiting = false - for _,buddyName in pairs(global.ocore.waitingBuddies) do - if (buddyChoice == buddyName) then - if (game.players[buddyChoice]) then - buddyIsStillWaiting = true - end - break - end - end - if (not buddyIsStillWaiting) then - player.print({"oarc-buddy-not-avail"}) - player.gui.screen.buddy_spawn_opts.destroy() - DisplayBuddySpawnOptions(player) - return - end + -- 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 + player.print({ "oarc-no-ungenerated-land-error" }) + return + end - if (global.ocfg.enable_separate_teams) then - joinMainTeamRadio = buddySpawnGui.buddy_spawn_main_team_radio.state - joinOwnTeamRadio = buddySpawnGui.buddy_spawn_new_team_radio.state - joinBuddyTeamRadio = buddySpawnGui.buddy_spawn_buddy_team_radio.state - else - joinMainTeamRadio = true - joinOwnTeamRadio = false - joinBuddyTeamRadio = false - end - if (global.ocfg.spawn_config.gen_settings.moat_choice_enabled) then - moatChoice = buddySpawnGui.buddy_spawn_moat_option_checkbox.state - end + -- Create a new force for player if they choose that radio button + if spawn_choices.team ~= SPAWN_TEAM_CHOICE.join_main_team then + CreatePlayerCustomForce(player) + end - -- Save the chosen spawn options somewhere for later use. - global.ocore.buddySpawnOpts[player.name] = {joinMainTeamRadio=joinMainTeamRadio, - joinOwnTeamRadio=joinOwnTeamRadio, - joinBuddyTeamRadio=joinBuddyTeamRadio, - moatChoice=moatChoice, - buddyChoice=buddyChoice, - distChoice=elemName} - - player.gui.screen.buddy_spawn_opts.destroy() - - -- Display prompts to the players - DisplayBuddySpawnWaitMenu(player) - DisplayBuddySpawnRequestMenu(game.players[buddyChoice], player.name) - if (game.players[buddyChoice].gui.screen.buddy_spawn_opts ~= nil) then - game.players[buddyChoice].gui.screen.buddy_spawn_opts.destroy() - end + -- Create that player's spawn in the global vars + SetPlayerRespawn(player.name, spawn_choices.surface, spawn_position, true) - -- Remove them from the buddy list while they make up their minds. - for i=#global.ocore.waitingBuddies,1,-1 do - name = global.ocore.waitingBuddies[i] - if ((name == player.name) or (name == buddyChoice)) then - global.ocore.waitingBuddies[i] = nil - end - end + -- Send the player there + QueuePlayerForDelayedSpawn(player.name, spawn_choices.surface, spawn_position, spawn_choices.moat, true, nil) + SendBroadcastMsg({"", { "oarc-player-is-joining", player.name, spawn_choices.surface }, " ", GetGPStext(spawn_choices.surface, spawn_position)}) - else - return -- Do nothing, no valid element item was clicked. + -- Unlock spawn control gui tab + SetOarcGuiTabEnabled(player, OARC_SPAWN_CTRL_TAB_NAME, true) + + -- Destroy the spawn options gui + if (player.gui.screen.spawn_opts ~= nil) then + player.gui.screen.spawn_opts.destroy() end end +---Display shared spawn join wait menu to the requesting player +---@param player LuaPlayer +---@return nil +function DisplaySharedSpawnJoinWaitMenu(player) -function DisplayBuddySpawnWaitMenu(player) + if (player.gui.screen.spawn_opts ~= nil) then + player.gui.screen.spawn_opts.destroy() + end + + local sGui = player.gui.screen.add { + name = "join_shared_spawn_wait_menu", + type = "frame", + direction = "vertical", + caption = { "oarc-waiting-for-spawn-owner" } + } - local sGui = player.gui.screen.add{name = "buddy_wait_menu", - type = "frame", - direction = "vertical", - caption={"oarc-waiting-for-buddy"}} sGui.auto_center = true sGui.style.maximal_width = SPAWN_GUI_MAX_WIDTH sGui.style.maximal_height = SPAWN_GUI_MAX_HEIGHT + sGui.style.padding = 5 + local sGui_if = sGui.add { + type = "frame", + direction = "vertical", + style = "inside_shallow_frame_with_padding" + } -- Warnings and explanations... - AddLabel(sGui, "warning_lbl1", {"oarc-wait-buddy-select-yes"}, my_warning_style) - AddSpacer(sGui) - sGui.add{name = "cancel_buddy_wait_menu", - type = "button", - caption={"oarc-cancel-return-to-previous"}} + AddLabel(sGui_if, "warning_lbl1", { "oarc-you-will-spawn-once-host" }, my_note_style) + + + local button_flow = sGui.add { + type = "flow", + direction = "horizontal", + style = "dialog_buttons_horizontal_flow" + } + button_flow.style.horizontally_stretchable = true + + local cancel_button = button_flow.add { + name = "cancel_shared_spawn_wait_menu", + type = "button", + tags = { action = "oarc_spawn_options", setting = "cancel_shared_spawn_wait_menu" }, + caption = { "oarc-cancel-button-caption" }, + tooltip = { "oarc-return-to-previous-tooltip" }, + style = "back_button" + } + + local dragger = button_flow.add{type="empty-widget", style="draggable_space_with_no_right_margin"} + dragger.style.horizontally_stretchable = true + dragger.style.height = 30 end --- Handle the gui click of the buddy wait menu -function BuddySpawnWaitMenuClick(event) - if not (event and event.element and event.element.valid) then return end - local player = game.players[event.player_index] - local elemName = event.element.name - if not player then - log("Another gui click happened with no valid player...") - return - end +---Display the buddy spawn wait menu +---@param player LuaPlayer +---@return nil +function DisplayBuddySpawnWaitMenu(player) - if (player.gui.screen.buddy_wait_menu == nil) then - return -- Gui event unrelated to this gui. + if (player.gui.screen.spawn_opts ~= nil) then + player.gui.screen.spawn_opts.destroy() end - -- Check if player is cancelling the request. - if (elemName == "cancel_buddy_wait_menu") then - player.gui.screen.buddy_wait_menu.destroy() - DisplaySpawnOptions(player) + local buddy_wait_menu = player.gui.screen.add { + name = "buddy_wait_menu", + type = "frame", + direction = "vertical", + caption = { "oarc-waiting-for-buddy" } + } - local buddy = game.players[global.ocore.buddySpawnOpts[player.name].buddyChoice] + buddy_wait_menu.auto_center = true + buddy_wait_menu.style.maximal_width = SPAWN_GUI_MAX_WIDTH + buddy_wait_menu.style.maximal_height = SPAWN_GUI_MAX_HEIGHT + buddy_wait_menu.style.padding = 5 - -- Catch a case where the buddy has left the game early and no longer exists. - if (buddy == nil) then - return - end + local buddy_wait_menu_if = buddy_wait_menu.add { + type = "frame", + direction = "vertical", + style = "inside_shallow_frame_with_padding" + } - if (buddy.gui.screen.buddy_request_menu ~= nil) then - buddy.gui.screen.buddy_request_menu.destroy() - end - if (buddy.gui.screen.buddy_spawn ~= nil) then - buddy.gui.screen.buddy_spawn_opts.destroy() - end - DisplaySpawnOptions(buddy) + -- Warnings and explanations... + AddLabel(buddy_wait_menu_if, nil, { "oarc-wait-buddy-select-yes" }, my_note_style) + AddSpacer(buddy_wait_menu_if) + + local button_flow = buddy_wait_menu.add { + type = "flow", + direction = "horizontal", + style = "dialog_buttons_horizontal_flow" + } + button_flow.style.horizontally_stretchable = true - buddy.print({"oarc-buddy-cancel-request", player.name}) - end + local cancel_button = button_flow.add { + name = "cancel_buddy_wait_menu", + tags = { action = "oarc_spawn_options", setting = "cancel_buddy_wait_menu" }, + type = "button", + style = "back_button", + caption = { "oarc-cancel-button-caption" }, + tooltip = { "oarc-return-to-previous-tooltip" }, + } + + local dragger = button_flow.add{type="empty-widget", style="draggable_space_with_no_right_margin"} + dragger.style.horizontally_stretchable = true + dragger.style.height = 30 end -function DisplayBuddySpawnRequestMenu(player, requestingBuddyName) - if not player then - log("Another gui click happened with no valid player...") - return +---Display the buddy spawn request menu +---@param player LuaPlayer The player that is receiving the buddy request +---@param requesting_buddy_name string The name of the player that is requesting to buddy spawn +---@return nil +function DisplayBuddySpawnRequestMenu(player, requesting_buddy_name) + + if (player.gui.screen.spawn_opts ~= nil) then + player.gui.screen.spawn_opts.destroy() end - local sGui = player.gui.screen.add{name = "buddy_request_menu", - type = "frame", - direction = "vertical", - caption="Buddy Request!"} - sGui.auto_center = true - sGui.style.maximal_width = SPAWN_GUI_MAX_WIDTH - sGui.style.maximal_height = SPAWN_GUI_MAX_HEIGHT + local buddy_request_gui = player.gui.screen.add { + name = "buddy_request_menu", + type = "frame", + direction = "vertical", + caption = { "oarc-buddy-spawn-request-header" } + } + buddy_request_gui.auto_center = true + buddy_request_gui.style.maximal_width = SPAWN_GUI_MAX_WIDTH + buddy_request_gui.style.maximal_height = SPAWN_GUI_MAX_HEIGHT + buddy_request_gui.style.padding = 5 + + local buddy_request_gui_if = buddy_request_gui.add { + type = "frame", + direction = "vertical", + style = "inside_shallow_frame_with_padding" + } -- Warnings and explanations... - AddLabel(sGui, "warning_lbl1", {"oarc-buddy-requesting-from-you", requestingBuddyName}, my_warning_style) + AddLabel(buddy_request_gui_if, nil, { "oarc-buddy-requesting-from-you", requesting_buddy_name }, my_note_style) + AddSpacer(buddy_request_gui_if) + + ---@type OarcSpawnChoices + local spawn_choices = global.spawn_choices[requesting_buddy_name] + ---@type LocalisedString local teamText = "error!" - if (global.ocore.buddySpawnOpts[requestingBuddyName].joinMainTeamRadio) then - teamText = {"oarc-buddy-txt-main-team"} - elseif (global.ocore.buddySpawnOpts[requestingBuddyName].joinOwnTeamRadio) then - teamText = {"oarc-buddy-txt-new-teams"} - elseif (global.ocore.buddySpawnOpts[requestingBuddyName].joinBuddyTeamRadio) then - teamText = {"oarc-buddy-txt-buddy-team"} + if (spawn_choices.buddy_team) then + teamText = { "oarc-buddy-txt-buddy-team" } + elseif (spawn_choices.team == SPAWN_TEAM_CHOICE.join_main_team) then + teamText = { "oarc-buddy-txt-main-team" } + elseif (spawn_choices.team == SPAWN_TEAM_CHOICE.join_own_team) then + teamText = { "oarc-buddy-txt-new-teams" } end + + ---@type LocalisedString local moatText = " " - if (global.ocore.buddySpawnOpts[requestingBuddyName].moatChoice) then - moatText = {"oarc-buddy-txt-moat"} + if (spawn_choices.moat) then + moatText = { "oarc-buddy-txt-moat" } end - local distText = "error!" - if (global.ocore.buddySpawnOpts[requestingBuddyName].distChoice == "buddy_spawn_request_near") then - distText = {"oarc-buddy-txt-near"} - elseif (global.ocore.buddySpawnOpts[requestingBuddyName].distChoice == "buddy_spawn_request_far") then - distText = {"oarc-buddy-txt-far"} - end + ---@type LocalisedString + local surfaceText = { "oarc-buddy-txt-surface", spawn_choices.surface} + ---@type LocalisedString + local distText = { "oarc-buddy-txt-distance", spawn_choices.distance} - local requestText = {"", requestingBuddyName, {"oarc-buddy-txt-would-like"}, teamText, {"oarc-buddy-txt-next-to-you"}, moatText, distText} - AddLabel(sGui, "note_lbl1", requestText, my_warning_style) - AddSpacer(sGui) + ---@type LocalisedString + local requestText = { "", requesting_buddy_name, { "oarc-buddy-txt-would-like" }, teamText, { "oarc-buddy-txt-next-to-you" }, + moatText, surfaceText, distText } + AddLabel(buddy_request_gui_if, nil, requestText, my_label_style) - sGui.add{name = "accept_buddy_request", - type = "button", - caption={"oarc-accept"}} - sGui.add{name = "decline_buddy_request", - type = "button", - caption={"oarc-reject"}} -end --- Handle the gui click of the buddy request menu -function BuddySpawnRequestMenuClick(event) - if not (event and event.element and event.element.valid) then return end - local player = game.players[event.player_index] - local elemName = event.element.name - local requesterName = nil - local requesterOptions = {} + local button_flow = buddy_request_gui.add { + type = "flow", + direction = "horizontal", + style = "dialog_buttons_horizontal_flow" + } + button_flow.style.horizontally_stretchable = true - if not player then - log("Another gui click happened with no valid player...") - return - end + local reject_button = button_flow.add { + name = "reject_buddy_request", + tags = { action = "oarc_spawn_options", setting = "reject_buddy_request", requesting_buddy_name = requesting_buddy_name }, + type = "button", + style = "red_back_button", + caption = { "oarc-reject" } + } + reject_button.style.horizontal_align = "left" + + local dragger = button_flow.add{type="empty-widget", style="draggable_space"} + dragger.style.horizontally_stretchable = true + dragger.style.height = 30 + + local accept_button = button_flow.add { + name = "accept_buddy_request", + tags = { action = "oarc_spawn_options", setting = "accept_buddy_request", requesting_buddy_name = requesting_buddy_name }, + type = "button", + style = "confirm_button", + caption = { "oarc-accept" } + } + accept_button.style.horizontal_align = "right" - if (player.gui.screen.buddy_request_menu == nil) then - return -- Gui event unrelated to this gui. - end +end +---Handles spawning the buddies together once the request has been accepted +---@param player LuaPlayer The player that is accepting the buddy request +---@param requesting_buddy_name string The name of the player that is requesting to buddy spawn +---@return nil +function AcceptBuddyRequest(player, requesting_buddy_name) - -- Check if it's a button press and lookup the matching buddy info - if ((elemName == "accept_buddy_request") or (elemName == "decline_buddy_request")) then - for name,opts in pairs(global.ocore.buddySpawnOpts) do - if (opts.buddyChoice == player.name) then - requesterName = name - requesterOptions = opts - end - end + ---@type OarcSpawnChoices + local spawn_choices = global.spawn_choices[requesting_buddy_name] + local requesting_buddy = game.players[requesting_buddy_name] + local surface = game.surfaces[spawn_choices.surface] - -- Not sure about this error condition... - if (requesterName == nil) then - SendBroadcastMsg("Error! Invalid buddy info???") - log("Error! Invalid buddy info...") + global.spawn_choices[player.name].host = nil -- N/A for buddy spawns so clear it. - player.gui.screen.buddy_request_menu.destroy() - DisplaySpawnOptions(player) - return - end - else - return -- Not a button click + -- Find coordinates of a good place to spawn + local spawn_position = FindUngeneratedCoordinates(surface, 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 + player.print({ "oarc-no-ungenerated-land-error" }) + return end - -- Handle player accepted - if (elemName == "accept_buddy_request") then + -- Create a new force for the combined players if they chose that option + if spawn_choices.buddy_team then + local buddyForce = CreatePlayerCustomForce(requesting_buddy) + player.force = buddyForce + -- Create a new force for each player if they chose that option + elseif spawn_choices.team == SPAWN_TEAM_CHOICE.join_own_team then + CreatePlayerCustomForce(player) + CreatePlayerCustomForce(requesting_buddy) + end - if (game.players[requesterName].gui.screen.buddy_wait_menu ~= nil) then - game.players[requesterName].gui.screen.buddy_wait_menu.destroy() - end - if (player.gui.screen.buddy_request_menu ~= nil) then - player.gui.screen.buddy_request_menu.destroy() - end + -- Destroy GUIs + if (requesting_buddy.gui.screen.buddy_wait_menu ~= nil) then + requesting_buddy.gui.screen.buddy_wait_menu.destroy() + end + if (player.gui.screen.buddy_request_menu ~= nil) then + player.gui.screen.buddy_request_menu.destroy() + end - -- Create a new spawn point - local newSpawn = {x=0,y=0} + -- Create that spawn in the global vars + local buddySpawn = { x = 0, y = 0 } + -- The x_offset must be big enough to ensure the spawns DO NOT overlap! + local x_offset = (global.ocfg.spawn_general.spawn_radius_tiles * 2) + if (spawn_choices.moat) then + x_offset = x_offset + 10 + end + buddySpawn = { x = spawn_position.x + x_offset, y = spawn_position.y } + SetPlayerRespawn(player.name, spawn_choices.surface, spawn_position, true) + SetPlayerRespawn(requesting_buddy_name, spawn_choices.surface, buddySpawn, true) - -- Create a new force for each player if they chose that option - if requesterOptions.joinOwnTeamRadio then - local newForce = CreatePlayerCustomForce(player) - local buddyForce = CreatePlayerCustomForce(game.players[requesterName]) + -- Send the player there + QueuePlayerForDelayedSpawn(player.name, spawn_choices.surface, spawn_position, spawn_choices.moat, true, requesting_buddy_name) + QueuePlayerForDelayedSpawn(requesting_buddy_name, spawn_choices.surface, buddySpawn, spawn_choices.moat, true, player.name) + SendBroadcastMsg({"", {"oarc-buddies-are-joining", requesting_buddy_name, player.name, spawn_choices.surface}, " ", GetGPStext(spawn_choices.surface, spawn_position)}) - -- Create a new force for the combined players if they chose that option - elseif requesterOptions.joinBuddyTeamRadio then - local buddyForce = CreatePlayerCustomForce(game.players[requesterName]) - player.force = buddyForce - end + -- Unlock spawn control gui tab + SetOarcGuiTabEnabled(player, OARC_SPAWN_CTRL_TAB_NAME, true) + SetOarcGuiTabEnabled(requesting_buddy, OARC_SPAWN_CTRL_TAB_NAME, true) - -- Find coordinates of a good place to spawn - if (requesterOptions.distChoice == "buddy_spawn_request_far") then - newSpawn = FindUngeneratedCoordinates(global.ocfg.far_dist_start,global.ocfg.far_dist_end, player.surface) - elseif (requesterOptions.distChoice == "buddy_spawn_request_near") then - newSpawn = FindUngeneratedCoordinates(global.ocfg.near_dist_start,global.ocfg.near_dist_end, player.surface) - end + global.buddy_pairs[player.name] = requesting_buddy_name + global.buddy_pairs[requesting_buddy_name] = player.name +end - -- If that fails, find a random map edge in a rand direction. - if ((newSpawn.x == 0) and (newSpawn.x == 0)) then - newSpawn = FindMapEdge(GetRandomVector(), player.surface) - log("Resorting to find map edge! x=" .. newSpawn.x .. ",y=" .. newSpawn.y) - end +---Rejects a buddy spawn request proposal +---@param player LuaPlayer The player that is rejecting the buddy request +---@param requesting_buddy_name string The name of the player that is requesting to buddy spawn +---@return nil +function RejectBuddyRequest(player, requesting_buddy_name) - -- Create that spawn in the global vars - local buddySpawn = {x=0,y=0} - if (requesterOptions.moatChoice) then - buddySpawn = {x=newSpawn.x+(global.ocfg.spawn_config.gen_settings.land_area_tiles*2)+10, y=newSpawn.y} - else - buddySpawn = {x=newSpawn.x+(global.ocfg.spawn_config.gen_settings.land_area_tiles*2), y=newSpawn.y} - end - ChangePlayerSpawn(player, newSpawn) - ChangePlayerSpawn(game.players[requesterName], buddySpawn) - - -- Send the player there - QueuePlayerForDelayedSpawn(player.name, newSpawn, requesterOptions.moatChoice, false) - QueuePlayerForDelayedSpawn(requesterName, buddySpawn, requesterOptions.moatChoice, false) - SendBroadcastMsg(requesterName .. " and " .. player.name .. " are joining the game together!") - - -- Unlock spawn control gui tab - SetOarcGuiTabEnabled(player, OARC_SPAWN_CTRL_GUI_NAME, true) - SetOarcGuiTabEnabled(game.players[requesterName], OARC_SPAWN_CTRL_GUI_NAME, true) - - player.print({"oarc-please-wait"}) - player.print({"", {"oarc-please-wait"}, "!"}) - player.print({"", {"oarc-please-wait"}, "!!"}) - game.players[requesterName].print({"oarc-please-wait"}) - game.players[requesterName].print({"", {"oarc-please-wait"}, "!"}) - game.players[requesterName].print({"", {"oarc-please-wait"}, "!!"}) - - global.ocore.buddyPairs[player.name] = requesterName - global.ocore.buddyPairs[requesterName] = player.name - - -- Check if player is cancelling the request. - elseif (elemName == "decline_buddy_request") then - player.gui.screen.buddy_request_menu.destroy() - DisplaySpawnOptions(player) + player.gui.screen.buddy_request_menu.destroy() + DisplaySpawnOptions(player) - local requesterBuddy = game.players[requesterName] + local requester_buddy = game.players[requesting_buddy_name] - if (requesterBuddy.gui.screen.buddy_wait_menu ~= nil) then - requesterBuddy.gui.screen.buddy_wait_menu.destroy() - end - if (requesterBuddy.gui.screen.buddy_spawn ~= nil) then - requesterBuddy.gui.screen.buddy_spawn_opts.destroy() - end - DisplaySpawnOptions(requesterBuddy) + if (requester_buddy == nil) then + return + end - requesterBuddy.print({"oarc-buddy-declined", player.name}) + if (requester_buddy.gui.screen.buddy_wait_menu ~= nil) then + requester_buddy.gui.screen.buddy_wait_menu.destroy() + DisplaySpawnOptions(requester_buddy) end - global.ocore.buddySpawnOpts[requesterName] = nil + requester_buddy.print({ "oarc-buddy-declined", player.name }) end - -function DisplayPleaseWaitForSpawnDialog(player, delay_seconds) - - local pleaseWaitGui = player.gui.screen.add{name = "wait_for_spawn_dialog", - type = "frame", - direction = "vertical", - caption={"oarc-spawn-wait"}} +---Display the please wait dialog +---@param player LuaPlayer +---@param delay_seconds integer +---@param surface LuaSurface +---@param position MapPosition +---@return nil +function DisplayPleaseWaitForSpawnDialog(player, delay_seconds, surface, position) + local pleaseWaitGui = player.gui.screen.add { name = "wait_for_spawn_dialog", + type = "frame", + direction = "vertical", + caption = { "oarc-spawn-wait" } } pleaseWaitGui.auto_center = true pleaseWaitGui.style.maximal_width = SPAWN_GUI_MAX_WIDTH pleaseWaitGui.style.maximal_height = SPAWN_GUI_MAX_HEIGHT - -- Warnings and explanations... - local wait_warning_text = {"oarc-wait-text", delay_seconds} + local wait_warning_text = { "oarc-wait-text", delay_seconds } AddLabel(pleaseWaitGui, "warning_lbl1", wait_warning_text, my_warning_style) + + local pleaseWaitGui_if = pleaseWaitGui.add { + type = "frame", + direction = "vertical", + style = "inside_shallow_frame_with_padding" + } + + pleaseWaitGui_if.add { + type = "minimap", + position = position, + surface_index = surface.index, + force = player.force.name + } +end + +---Get a list of OTHER players currently in the spawn menu +---@param self_player LuaPlayer +---@return table +function GetOtherPlayersInSpawnMenu(self_player) + local other_players_in_spawn_menu = {} + for _, player in pairs(game.connected_players) do + if (player.gui.screen.spawn_opts ~= nil) and (self_player ~= player) then + table.insert(other_players_in_spawn_menu, player.name) + end + end + return other_players_in_spawn_menu +end + +---Gui click event handlers +---@param event EventData.on_gui_click +---@return nil +function SeparateSpawnsGuiClick(event) + WelcomeTextGuiClick(event) + SpawnOptsGuiClick(event) +end + +---Gui checked state changed event handlers +---@param event EventData.on_gui_checked_state_changed +---@return nil +function SeparateSpawnsGuiCheckedStateChanged(event) + SpawnOptsRadioSelect(event) +end + +---Gui value changed event handlers +---@param event EventData.on_gui_value_changed +---@return nil +function SeparateSpawnsGuiValueChanged(event) + SpawnOptsValueChanged(event) +end + +---Gui selection state changed event handlers +---@param event EventData.on_gui_selection_state_changed +---@return nil +function SeparateSpawnsGuiSelectionStateChanged(event) + SpawnOptsSelectionChanged(event) end \ No newline at end of file diff --git a/lib/shared_chests.lua b/lib/shared_chests.lua deleted file mode 100644 index e2d3469..0000000 --- a/lib/shared_chests.lua +++ /dev/null @@ -1,689 +0,0 @@ --- shared_chests.lua --- Feb 2020 --- Oarc's silly idea for a scripted item sharing solution. - --- Buffer size is the limit of joules/tick so multiply by 60 to get /sec. -SHARED_ELEC_OUTPUT_BUFFER_SIZE = 1000000000 -SHARED_ELEC_INPUT_BUFFER_SIZE = 1000000001 - -SHARED_ENERGY_STARTING_VALUE = 0 -- 100GJ - -function SharedChestInitItems() - - global.oshared = {} - - global.oshared.chests = {} - global.oshared.requests = {} - global.oshared.requests_totals = {} - - global.oshared.electricity_inputs = {} - global.oshared.electricity_outputs = {} - - global.oshared.chests_combinators = {} - global.oshared.items = {} - - global.oshared.items['red-wire'] = 10000 - global.oshared.items['green-wire'] = 10000 - global.oshared.items['raw-fish'] = 10000 - - global.oshared.energy_stored = SHARED_ENERGY_STARTING_VALUE - global.oshared.energy_stored_history = {start=SHARED_ENERGY_STARTING_VALUE, after_input=SHARED_ENERGY_STARTING_VALUE, after_output=SHARED_ENERGY_STARTING_VALUE} -end - -function SharedEnergySpawnInput(player, pos) - - local inputElec = game.surfaces[GAME_SURFACE_NAME].create_entity{name="electric-energy-interface", position=pos, force="neutral"} - inputElec.destructible = false - inputElec.minable = false - inputElec.operable = false - inputElec.last_user = player - - inputElec.electric_buffer_size = SHARED_ELEC_INPUT_BUFFER_SIZE - inputElec.power_production = 0 - inputElec.power_usage = 0 - inputElec.energy = 0 - - local inputElecCombi = game.surfaces[GAME_SURFACE_NAME].create_entity{name="constant-combinator", position={x=pos.x+1, y=pos.y}, force="neutral"} - inputElecCombi.destructible = false - inputElecCombi.minable = false - inputElecCombi.operable = true -- Input combi can be set by the player! - inputElecCombi.last_user = player - - -- Default share is 1MW - inputElecCombi.get_or_create_control_behavior().set_signal(1, - {signal={type="virtual", name="signal-M"}, - count=1}) - - TemporaryHelperText("Connect to electric network to contribute shared energy.", {pos.x+1.5, pos.y-1}, TICKS_PER_MINUTE*2) - TemporaryHelperText("Use combinator to limit number of MW shared.", {pos.x+2.5, pos.y}, TICKS_PER_MINUTE*2) - - table.insert(global.oshared.electricity_inputs, {eei=inputElec, combi=inputElecCombi}) -end - -function SharedEnergySpawnOutput(player, pos) - - local outputElec = game.surfaces[GAME_SURFACE_NAME].create_entity{name="electric-energy-interface", position=pos, force="neutral"} - outputElec.destructible = false - outputElec.minable = false - outputElec.operable = false - outputElec.last_user = player - - outputElec.electric_buffer_size = SHARED_ELEC_OUTPUT_BUFFER_SIZE - outputElec.power_production = 0 - outputElec.power_usage = 0 - outputElec.energy = 0 - - local outputElecCombi = game.surfaces[GAME_SURFACE_NAME].create_entity{name="constant-combinator", position={x=pos.x+1, y=pos.y}, force="neutral"} - outputElecCombi.destructible = false - outputElecCombi.minable = false - outputElecCombi.operable = false -- Output combi is set my script! - outputElec.last_user = player - - TemporaryHelperText("Connect to electric network to consume shared energy.", {pos.x+1.5, pos.y-1}, TICKS_PER_MINUTE*2) - TemporaryHelperText("Combinator outputs number of MJ currently stored.", {pos.x+2.5, pos.y}, TICKS_PER_MINUTE*2) - - table.insert(global.oshared.electricity_outputs, {eei=outputElec, combi=outputElecCombi}) -end - -function SharedEnergyStoreInputOnTick() - global.oshared.energy_stored_history.start = global.oshared.energy_stored - - for idx,input in pairs(global.oshared.electricity_inputs) do - - -- Check for entity no longer valid: - if (input.eei == nil) or (not input.eei.valid) or (input.combi == nil) or (not input.combi.valid) then - global.oshared.electricity_inputs[idx] = nil - - -- Is input at least half full, then we can start to store energy. - elseif (input.eei.energy > (SHARED_ELEC_INPUT_BUFFER_SIZE/2)) then - - -- Calculate the max we can share - local max_input_allowed = input.eei.energy - (SHARED_ELEC_INPUT_BUFFER_SIZE/2) - - -- Get the combinator limit - local limit = 0 - local sig = input.combi.get_or_create_control_behavior().get_signal(1) - if ((sig ~= nil) and (sig.signal ~= nil) and (sig.signal.name == "signal-M")) then - limit = sig.count - end - - -- Get the minimum - input.eei.power_usage = math.min(max_input_allowed, math.floor(limit*1000000/60)) - - global.oshared.energy_stored = global.oshared.energy_stored + input.eei.power_usage - - -- Switch off contribution if not at least half full. - else - input.eei.power_usage = 0 - end - end - - global.oshared.energy_stored_history.after_input = global.oshared.energy_stored -end - --- If there is room to distribute energy, we take shared amount split by players. -function SharedEnergyDistributeOutputOnTick() - - -- Share limit is total amount stored divided by outputs - local energyShareCap = math.floor(global.oshared.energy_stored / (#global.oshared.electricity_outputs)) - - -- Iterate through and fill up outputs if they are under 50% - for idx,output in pairs(global.oshared.electricity_outputs) do - - -- Check for entity no longer valid: - if (output.eei == nil) or (not output.eei.valid) or (output.combi == nil) or (not output.combi.valid) then - global.oshared.electricity_outputs[idx] = nil - - - else - -- If it's not full, set production to fill (or as much as is allowed.) - if (output.eei.energy < (SHARED_ELEC_OUTPUT_BUFFER_SIZE/2)) then - local outBufferSpace = ((SHARED_ELEC_OUTPUT_BUFFER_SIZE/2) - output.eei.energy) - output.eei.power_production = math.min(outBufferSpace, energyShareCap) - global.oshared.energy_stored = global.oshared.energy_stored - math.min(outBufferSpace, energyShareCap) - - -- Switch off if we're more than half full. - else - output.eei.power_production = 0 - end - - -- Update output combinator - output.combi.get_or_create_control_behavior().set_signal(1, - {signal={type="virtual", name="signal-M"}, - count=clampInt32(math.floor(global.oshared.energy_stored/1000000))}) - end - end - - global.oshared.energy_stored_history.after_output = global.oshared.energy_stored -end - --- Returns NIL or position of destroyed chest. -function FindClosestWoodenChestAndDestroy(player) - local target_chest = FindClosestPlayerOwnedEntity(player, "wooden-chest", 16) - if (not target_chest) then - player.print("Failed to find wooden-chest?") - return nil - end - - if (not target_chest.get_inventory(defines.inventory.chest).is_empty()) then - player.print("Chest is NOT empty! Please empty it and try again.") - return nil - end - - local pos = target_chest.position - if (not target_chest.destroy()) then - player.print("ERROR - Can't remove wooden chest??") - return nil - end - - return {x=math.floor(pos.x),y=math.floor(pos.y)} -end - -function ConvertWoodenChestToSharedChestInput(player) - local pos = FindClosestWoodenChestAndDestroy(player) - if (pos) then - SharedChestsSpawnInput(player, pos) - OarcMapFeaturePlayerCountChange(player, "special_chests", "logistic-chest-storage", 1) - return true - end - return false -end - -function ConvertWoodenChestToSharedChestOutput(player) - local pos = FindClosestWoodenChestAndDestroy(player) - if (pos) then - SharedChestsSpawnOutput(player, pos) - OarcMapFeaturePlayerCountChange(player, "special_chests", "logistic-chest-requester", 1) - return true - end - return false -end - -function ConvertWoodenChestToSharedChestCombinators(player) - local pos = FindClosestWoodenChestAndDestroy(player) - if (pos) then - if (player.surface.can_place_entity{name="constant-combinator", position={pos.x,pos.y-1}}) and - (player.surface.can_place_entity{name="constant-combinator", position={pos.x,pos.y+1}}) then - SharedChestsSpawnCombinators(player, {x=pos.x,y=pos.y-1}, {x=pos.x,y=pos.y+1}) - return true - else - player.print("Failed to place the special combinators. Please check there is enough space in the surrounding tiles!") - end - end - return false -end - -function ConvertWoodenChestToShareEnergyInput(player) - local pos = FindClosestWoodenChestAndDestroy(player) - if (pos) then - if (player.surface.can_place_entity{name="electric-energy-interface", position=pos}) and - (player.surface.can_place_entity{name="constant-combinator", position={x=pos.x+1, y=pos.y}}) then - SharedEnergySpawnInput(player, pos) - OarcMapFeaturePlayerCountChange(player, "special_chests", "accumulator", 1) - return true - else - player.print("Failed to place the shared energy input. Please check there is enough space in the surrounding tiles!") - end - end - return false -end - -function ConvertWoodenChestToShareEnergyOutput(player) - local pos = FindClosestWoodenChestAndDestroy(player) - if (pos) then - if (player.surface.can_place_entity{name="electric-energy-interface", position=pos}) and - (player.surface.can_place_entity{name="constant-combinator", position={x=pos.x+1, y=pos.y}}) then - SharedEnergySpawnOutput(player, pos) - OarcMapFeaturePlayerCountChange(player, "special_chests", "electric-energy-interface", 1) - return true - else - player.print("Failed to place the shared energy input. Please check there is enough space in the surrounding tiles!") - end - end - return false -end - -function ConvertWoodenChestToWaterFill(player) - local pos = FindClosestWoodenChestAndDestroy(player) - if (pos) then - if (getDistance(pos, player.position) > 2) then - player.surface.set_tiles({[1]={name = "water", position=pos}}) - return true - else - player.print("Failed to place waterfill. Don't stand so close FOOL!") - end - end - return false -end - -function DestroyClosestSharedChestEntity(player) - local special_entities = game.surfaces[GAME_SURFACE_NAME].find_entities_filtered{ - name={"electric-energy-interface", "constant-combinator", "logistic-chest-storage", "logistic-chest-requester"}, - position=player.position, - radius=16, - force={"neutral"}} - - if (#special_entities == 0) then - player.print("Special entity not found? Are you close enough?") - return - end - - local closest = game.surfaces[GAME_SURFACE_NAME].get_closest(player.position, special_entities) - - if (closest) then - if (closest.last_user and (closest.last_user ~= player)) then - player.print("You can't remove other players chests!") - else - -- Subtract from feature counter... - local name = closest.name - if (name == "electric-energy-interface") then - if (closest.electric_buffer_size == SHARED_ELEC_INPUT_BUFFER_SIZE) then - OarcMapFeaturePlayerCountChange(player, "special_chests", "accumulator", -1) - else - OarcMapFeaturePlayerCountChange(player, "special_chests", "electric-energy-interface", -1) - end - elseif (name == "logistic-chest-storage") then - OarcMapFeaturePlayerCountChange(player, "special_chests", "logistic-chest-storage", -1) - elseif (name == "logistic-chest-requester") then - OarcMapFeaturePlayerCountChange(player, "special_chests", "logistic-chest-requester", -1) - end - - closest.destroy() - player.print("Special entity removed!") - end - else - player.print("Special entity not found? Are you close enough? -- ERROR") - end -end - -function SharedChestsSpawnInput(player, pos) - - local inputChest = game.surfaces[GAME_SURFACE_NAME].create_entity{name="logistic-chest-storage", position={pos.x, pos.y}, force="neutral"} - inputChest.destructible = false - inputChest.minable = false - inputChest.last_user = player - - if global.oshared.chests == nil then - global.oshared.chests = {} - end - - local chestInfoIn = {player=player.name,type="INPUT",entity=inputChest} - table.insert(global.oshared.chests, chestInfoIn) - - TemporaryHelperText("Place items in to share.", {pos.x+1.5, pos.y}, TICKS_PER_MINUTE*2) -end - -function SharedChestsSpawnOutput(player, pos, enable_example) - - local outputChest = game.surfaces[GAME_SURFACE_NAME].create_entity{name="logistic-chest-requester", position={pos.x, pos.y}, force="neutral"} - outputChest.destructible = false - outputChest.minable = false - outputChest.last_user = player - - if (enable_example) then - outputChest.set_request_slot({name="raw-fish", count=1}, 1) - end - - if global.oshared.chests == nil then - global.oshared.chests = {} - end - - local chestInfoOut = {player=player.name,type="OUTPUT",entity=outputChest} - table.insert(global.oshared.chests, chestInfoOut) - - TemporaryHelperText("Set filters to request items.", {pos.x+1.5, pos.y}, TICKS_PER_MINUTE*2) -end - - -function SharedChestsSpawnCombinators(player, posCtrl, posStatus) - - local combiCtrl = game.surfaces[GAME_SURFACE_NAME].create_entity{name="constant-combinator", position=posCtrl, force="neutral"} - combiCtrl.destructible = false - combiCtrl.minable = false - combiCtrl.last_user = player - - -- Fish as an example. - combiCtrl.get_or_create_control_behavior().set_signal(1, {signal={type="item", name="raw-fish"}, count=1}) - - local combiStat = game.surfaces[GAME_SURFACE_NAME].create_entity{name="constant-combinator", position=posStatus, force="neutral"} - combiStat.destructible = false - combiStat.minable = false - combiStat.operable = false - combiStat.last_user = player - - if global.oshared.chests_combinators == nil then - global.oshared.chests_combinators = {} - end - - local combiPair = {player=player.name,ctrl=combiCtrl,status=combiStat} - table.insert(global.oshared.chests_combinators, combiPair) - - TemporaryHelperText("Set signals here to monitor item counts.", {posCtrl.x+1.5, posCtrl.y}, TICKS_PER_MINUTE*2) - TemporaryHelperText("Receive signals here to see available items.", {posStatus.x+1.5, posStatus.y}, TICKS_PER_MINUTE*2) -end - -function SharedChestsUpdateCombinators() - - if global.oshared.chests_combinators == nil then - global.oshared.chests_combinators = {} - end - - for idx,combiPair in pairs(global.oshared.chests_combinators) do - - -- Check if combinators still exist - if (combiPair.ctrl == nil) or (combiPair.status == nil) or - (not combiPair.ctrl.valid) or (not combiPair.status.valid) then - global.oshared.chests_combinators[idx] = nil - else - - local combiCtrlBehav = combiPair.ctrl.get_or_create_control_behavior() - local ctrlSignals = {} - - -- Get signals on the ctrl combi: - for i=1,combiCtrlBehav.signals_count do - local sig = combiCtrlBehav.get_signal(i) - if ((sig ~= nil) and (sig.signal ~= nil) and (sig.signal.type == "item")) then - table.insert(ctrlSignals, sig.signal.name) - end - end - - local combiStatBehav = combiPair.status.get_or_create_control_behavior() - - -- Set signals on the status combi: - for i=1,combiCtrlBehav.signals_count do - if (ctrlSignals[i] ~= nil) then - local availAmnt = global.oshared.items[ctrlSignals[i]] - if availAmnt == nil then availAmnt = 0 end - - combiStatBehav.set_signal(i, {signal={type="item", name=ctrlSignals[i]}, count=clampInt32(availAmnt)}) - else - combiStatBehav.set_signal(i, nil) - end - end - end - end -end - -function SharedChestUploadItem(item_name, count) - if (not game.item_prototypes[item_name].has_flag("hidden")) then - if (global.oshared.items[item_name] == nil) then - global.oshared.items[item_name] = count - else - global.oshared.items[item_name] = global.oshared.items[item_name] + count - end - return true - else - return false - end -end - -function SharedChestEmptyEquipment(item_stack) - if (item_stack == nil) then - return - end - - if (item_stack.grid == nil) then - return - end - - local contents = item_stack.grid.get_contents() - for item_name,count in pairs(contents) do - SharedChestUploadItem(item_name, count) - end -end - -function SharedChestUploadChest(entity) - - local chest_inv = entity.get_inventory(defines.inventory.chest) - if (chest_inv == nil) then return end - if (chest_inv.is_empty()) then return end - - local contents = chest_inv.get_contents() - for item_name,count in pairs(contents) do - if (game.item_prototypes[item_name].equipment_grid ~= nil) then - local item_stack = chest_inv.find_item_stack(item_name) - while (item_stack ~= nil) do - SharedChestEmptyEquipment(item_stack) - item_stack.clear() - item_stack = chest_inv.find_item_stack(item_name) - end - end - - if (SharedChestUploadItem(item_name, count)) then - chest_inv.remove({name=item_name, count=count}) - end - end -end - --- Pull all items in the deposit chests -function SharedChestsDepositAll() - - if global.oshared.items == nil then - global.oshared.items = {} - end - - for idx,chest_info in pairs(global.oshared.chests) do - - local chest_entity = chest_info.entity - - -- Delete any chest that is no longer valid. - if ((chest_entity == nil) or (not chest_entity.valid)) then - global.oshared.chests[idx] = nil - - -- Take inputs and store. - elseif (chest_info.type == "INPUT") then - SharedChestUploadChest(chest_entity) - end - end -end - --- Tally up requests by item. -function SharedChestsTallyRequests() - - -- Clear existing requests. Also serves as an init - global.oshared.requests = {} - global.oshared.requests_totals = {} - - -- For each output chest. - for idx,chestInfo in pairs(global.oshared.chests) do - - local chestEntity = chestInfo.entity - - -- Delete any chest that is no longer valid. - if ((chestEntity == nil) or (not chestEntity.valid)) then - global.oshared.chests[idx] = nil - - elseif (chestInfo.type == "OUTPUT") then - - -- For each request slot - for i = 1, chestEntity.request_slot_count, 1 do - local req = chestEntity.get_request_slot(i) - - -- If there is a request, add the request count to our request table. - if (req ~= nil) then - - if global.oshared.requests[req.name] == nil then - global.oshared.requests[req.name] = {} - end - - if global.oshared.requests[req.name][chestInfo.player] == nil then - global.oshared.requests[req.name][chestInfo.player] = 0 - end - - if global.oshared.requests_totals[req.name] == nil then - global.oshared.requests_totals[req.name] = 0 - end - - -- Calculate actual request to fill remainder - local existingAmount = chestEntity.get_inventory(defines.inventory.chest).get_item_count(req.name) - local requestAmount = math.max(req.count-existingAmount, 0) - - -- Add the request counts - global.oshared.requests[req.name][chestInfo.player] = global.oshared.requests[req.name][chestInfo.player] + requestAmount - global.oshared.requests_totals[req.name] = global.oshared.requests_totals[req.name] + requestAmount - end - end - end - end - - - -- If demand is more than supply, limit each player's total item request to shared amount - for reqName,reqTally in pairs(global.oshared.requests) do - - local cap = 0 - local mustCap = false - - -- No shared items means nothing to supply. - if (global.oshared.items[reqName] == nil) or (global.oshared.items[reqName] == 0) then - mustCap = true - cap = 0 - - -- Otherwise, limit by dividing by players. - elseif (global.oshared.requests_totals[reqName] > global.oshared.items[reqName]) then - mustCap = true - cap = math.floor(global.oshared.items[reqName] / TableLength(global.oshared.requests[reqName])) - - -- In the case where we are rounding down to 0, let's bump the minimum distribution to 1. - if (cap == 0) then - cap = 1 - end - end - - -- Limit each request to the cap. - if mustCap then - for player,reqCount in pairs(global.oshared.requests[reqName]) do - if (reqCount > cap) then - global.oshared.requests[reqName][player] = cap - end - end - end - end - -end - - --- Distribute requests based on demand -function SharedChestsDistributeRequests() - - -- For each output chest. - for idx,chestInfo in pairs(global.oshared.chests) do - if (chestInfo.type == "OUTPUT") then - - local chestEntity = chestInfo.entity - - -- Delete any chest that is no longer valid. - if ((chestEntity == nil) or (not chestEntity.valid)) then - global.oshared.chests[idx] = nil - - -- For each request slot - else - for i = 1, chestEntity.request_slot_count, 1 do - local req = chestEntity.get_request_slot(i) - - -- If there is a request, distribute items - if (req ~= nil) then - - -- Make sure requests have been created. - -- Make sure shared items exist. - if (global.oshared.requests_totals[req.name] ~= nil) and - (global.oshared.items[req.name] ~= nil) and - (global.oshared.requests[req.name][chestInfo.player] ~= nil) then - - if (global.oshared.requests[req.name][chestInfo.player] > 0)and (global.oshared.items[req.name] > 0) then - - -- How much is already in the chest? - local existingAmount = chestEntity.get_inventory(defines.inventory.chest).get_item_count(req.name) - -- How much is required to fill the remainder request? - local requestAmount = math.max(req.count-existingAmount, 0) - -- How much is allowed based on the player's current request amount? - local allowedAmount = math.min(requestAmount, global.oshared.requests[req.name][chestInfo.player]) - - if (allowedAmount > 0) then - local chestInv = chestEntity.get_inventory(defines.inventory.chest) - if chestInv.can_insert({name=req.name}) then - - local amnt = chestInv.insert({name=req.name, count=math.min(allowedAmount, global.oshared.items[req.name])}) - global.oshared.items[req.name] = global.oshared.items[req.name] - amnt - global.oshared.requests[req.name][chestInfo.player] = global.oshared.requests[req.name][chestInfo.player] - amnt - global.oshared.requests_totals[req.name] = global.oshared.requests_totals[req.name] - amnt - - end - end - end - end - end - end - end - end - end -end - -function SharedChestsOnTick() - - -- Every tick we share power - SharedEnergyStoreInputOnTick() - SharedEnergyDistributeOutputOnTick() - - -- Every second, we check the input chests and deposit stuff. - if ((game.tick % (60)) == 37) then - SharedChestsDepositAll() - end - - -- Every second, we check the output chests for requests - if ((game.tick % (60)) == 38) then - SharedChestsTallyRequests() - end - - -- Every second, we distribute to the output chests. - if ((game.tick % (60)) == 39) then - SharedChestsDistributeRequests() - end - - -- Every second, we update our combinator status info. - if ((game.tick % (60)) == 40) then - SharedChestsUpdateCombinators() - end - -end - - -function CreateSharedItemsGuiTab(tab_container, player) - local scrollFrame = tab_container.add{type="scroll-pane", - name="sharedItems-panel", - direction = "vertical"} - ApplyStyle(scrollFrame, my_shared_item_list_fixed_width_style) - scrollFrame.horizontal_scroll_policy = "never" - - AddLabel(scrollFrame, "share_items_info", "Place items into the [color=yellow]yellow storage chests to share[/color].\nRequest items from the [color=blue]blue requestor chests to pull out items[/color].\nTo refresh this view, click the tab again.\nShared items are accessible by [color=red]EVERYONE and all teams[/color].\nThe combinator pair allows you to 'set' item types to watch for. Set items in the top one, and connect the bottom one to a circuit network to view the current available inventory. Items with 0 amount do not generate any signal.\nThe special accumulators share energy. The top one acts as an input, the bottom is the output.", my_longer_label_style) - - AddSpacerLine(scrollFrame) - - -- MW charging/discharging rate. (delta change * sample rate per second) - local energy_change_add = (global.oshared.energy_stored_history.after_input - global.oshared.energy_stored_history.start)*60/1000000 - local energy_change_sub = (((global.oshared.energy_stored_history.after_input - global.oshared.energy_stored_history.after_output)*60))/1000000 - local energy_add_str = string.format("+%.3fMW", energy_change_add) - local energy_sub_str = string.format("-%.3fMW", energy_change_sub) - local rate_color = "green" - if (energy_change_add <= energy_change_sub) then - rate_color = "red" - elseif (energy_change_add < (energy_change_sub+10)) then - rate_color = "orange" - end - - AddLabel(scrollFrame, "elec_avail_info", "[color=acid]Current electricity available: " .. string.format("%.3f", global.oshared.energy_stored/1000000) .. "MJ[/color] [color=" .. rate_color .. "](" .. energy_add_str .. " " .. energy_sub_str ..")[/color]", my_longer_label_style) - - AddSpacerLine(scrollFrame) - AddLabel(scrollFrame, "share_items_title_msg", "Shared Items:", my_label_header_style) - - local sorted_items = {} - for k in pairs(global.oshared.items) do table.insert(sorted_items, k) end - table.sort(sorted_items) - - for idx,itemName in pairs(sorted_items) do - if (global.oshared.items[itemName] > 0) then - local caption_str = "[item="..itemName.."] " .. itemName..": "..global.oshared.items[itemName] - AddLabel(scrollFrame, itemName.."_itemlist", caption_str, my_player_list_style) - end - end - -end \ No newline at end of file diff --git a/lib/sharing.lua b/lib/sharing.lua new file mode 100644 index 0000000..a4fd7ed --- /dev/null +++ b/lib/sharing.lua @@ -0,0 +1,105 @@ +-- This handles the shared power logic for the Oarc scenario. +-- Won't work too hard on this since 2.0 might change things... + + +STARTING_X_OFFSET_SHARING_POLE = -5 +Y_OFFSET_SHARING_POLE = 20 + +---Create and connect a pair of power poles for a new base given surface and position. +---@param surface LuaSurface +---@param position MapPosition +---@return nil +function CreateSharedPowerPolePair(surface, position) + + if global.shared_power_poles == nil then + ---@type LuaEntity[] + global.shared_power_poles = {} + end + + --Get an open sharing pole from the holding pen surface if one exists, otherwise create a new one. + local hidden_pole = FindSharedPowerPole() + if not hidden_pole then + local poles_count = table_size(global.shared_power_poles) + local new_position = { x = poles_count + STARTING_X_OFFSET_SHARING_POLE, y = Y_OFFSET_SHARING_POLE } + hidden_pole = CreateSpecialPole(game.surfaces[HOLDING_PEN_SURFACE_NAME], new_position) + if not hidden_pole then + log("ERROR - Failed to create shared power poles!? " .. serpent.block(position) .. " on " .. surface.name) + return + end + table.insert(global.shared_power_poles, hidden_pole) + end + + --Create the base pole on the new spawn area surface and connect it to the hidden pole. + local base_pole = CreateSpecialPole(surface, position) + if not base_pole then + log("ERROR - Failed to create shared power poles!? " .. serpent.block(position) .. " on " .. surface.name) + return + end + base_pole.connect_neighbour(hidden_pole) + + TemporaryHelperText( + { "oarc-shared-power-pole-helper-txt" }, + surface, + {position.x, position.y}, + TICKS_PER_MINUTE*2, + "right" + ) +end + +---Find the first shared power pole that doesn't exceed the max number of connections. +---@return LuaEntity? +function FindSharedPowerPole() + if global.shared_power_poles == nil then return nil end + + for _,pole in pairs(global.shared_power_poles) do + -- 5 is the hard coded engine limit and we need to leave one open for the connection to the next hidden pole. + if pole.neighbours["copper"] and table_size(pole.neighbours["copper"]) < 4 then + return pole + end + end + + return nil +end + +---Creates a special pole on the surface at the given position on the neutral force. +---@param surface LuaSurface +---@param position MapPosition +---@return LuaEntity? +function CreateSpecialPole(surface, position) + local pole = surface.create_entity + { + name="oarc-linked-power", + position=position, + force="neutral" + } + pole.destructible = false + pole.minable = false + pole.rotatable = false + return pole +end + +---Creates a special linked-chest on the surface at the given position on the neutral force. +---@param surface LuaSurface +---@param position MapPosition +---@return LuaEntity? +function CreateSharedChest(surface, position) + local chest = surface.create_entity + { + name="oarc-linked-chest", + position=position, + force="neutral" + } + chest.destructible = false + chest.minable = false + chest.rotatable = false + + TemporaryHelperText( + { "oarc-shared-chest-helper-txt" }, + surface, + {position.x, position.y}, + TICKS_PER_MINUTE*2, + "right" + ) + + return chest +end \ No newline at end of file diff --git a/lib/tag.lua b/lib/tag.lua deleted file mode 100644 index a55125f..0000000 --- a/lib/tag.lua +++ /dev/null @@ -1,52 +0,0 @@ --- tag.lua --- Apr 2017 --- Allows adding play tags - --- Tag list -local roles = { - {display_name = "[Solo]"}, - {display_name = "[Mining]"}, - {display_name = "[Power]"}, - {display_name = "[Oil]"}, - {display_name = "[Smelt]"}, - {display_name = "[Rail]"}, - {display_name = "[Defense]"}, - {display_name = "[Circuits]"}, - {display_name = "[Science!]"}, - {display_name = "[Logistics]"}, - {display_name = "[Misc]"}, - {display_name = "[Aliens]"}, - {display_name = "[Rocket]"}, - {display_name = "[AFK]"}} - -function CreateTagGuiTab(tab_container, player) - for i,role in ipairs(roles) do - tab_container.add{type="button", caption=role.display_name, name=role.display_name} - end - if (player.admin) then - tab_container.add{type="button", caption="[Admin]", name="admin"} - tab_container.add{type="button", caption="[Moderator]", name="moderator"} - end - tab_container.add{type="button", caption="Clear", name="clear_btn"} -end - -function TagGuiClick(event) - if not (event and event.element and event.element.valid) then return end - local player = game.players[event.player_index] - local name = event.element.name - - if (name == "clear_btn") then - player.tag = "" - return - end - - for i,role in ipairs(roles) do - if (name == role.display_name) then - player.tag = role.display_name - elseif (name == "admin") then - player.tag = "[Admin]" - elseif (name == "moderator") then - player.tag = "[Moderator]" - end - end -end diff --git a/locale/de/locale.cfg b/locale/de/locale.cfg deleted file mode 100644 index 1325a47..0000000 --- a/locale/de/locale.cfg +++ /dev/null @@ -1,122 +0,0 @@ -oarc-spawn-time-warning-msg=Aufgrund der Funktionsweise dieses Szenarios kann es einige Zeit dauern, bis das Land um deinen neuen Spawn herum generiert wurde. Bitte warte 10-20 Sekunden, nachdem du deinen Spawn ausgewählt hast. - -oarc-i-understand=Ich verstehe -oarc-spawn-options=Spawn Optionen - -oarc-click-info-btn-help=Klick oben links auf die Schaltfläche INFO, um mehr über dieses Szenario zu erfahren! Dies ist deine einzige Chance, eine Spawn-Option zu wählen. Wähle sorgfältig... - -oarc-vanilla-spawn=Vanilla Spawn -oarc-default-spawn-behavior=Dies ist das standardmäßige Spawn-Verhalten eines Vanilla-Spiels. Du trittst dem Standardteam in der Mitte der Karte bei. - -oarc-join-main-team-radio=Dem Standardteam beitreten (gemeinsame Forschung) -oarc-create-own-team-radio=Erstelle dein eigenes Team (eigene Forschung) - -oarc-moat-option=Umgib deinen Spawn mit einem Graben. - -oarc-solo-spawn-near=Allein spawnen (nah) -oarc-solo-spawn-far=Allein spawnen (fern) - -oarc-starting-area-vanilla=Du spawnst in deinem eigenen Startbereich (wie im Vanilla-Spiel). -oarc-vanilla-spawns-available=Es steht __1__ Vanilla-Spawn zur Verfügung. - -oarc-starting-area-normal=Du spawnst in einem neuen Gebiet mit voreingestellten Startressourcen. -oarc-join-someone-avail=Jemandem beitreten (__1__ verfügbar) -oarc-join-someone-info=Du wirst in der Basis von jemand anderem spawnen. Dies erfordert, dass mindestens eine Person Zugang zu seiner Basis gewährt hat. Diese Wahl ist endgültig und du wirst später nicht mehr in der Lage sein, deinen eigenen Spawn zu erstellen. - -oarc-no-shared-avail=Es gibt derzeit keine freigegebenen Basen, in denen man spawnen kann. -oarc-join-check-again=Erneut prüfen -oarc-shared-spawn-disabled=Gemeinsame Spawns sind in diesem Modus deaktiviert. - -oarc-buddy-spawn=Buddy Spawn -oarc-buddy-spawn-info=Das Buddy-System erfordert 2 Spieler in diesem Menü gleichzeitig, sie spawnen nebeneinander, jeder mit seinen eigenen Ressourcen. - -oarc-max-players-shared-spawn=Wenn du deinen eigenen Spawn-Punkt erstellst, kann bis zu __1__ anderer Online-Spieler teilnehmen. -oarc-spawn-dist-notes=Der nahe Spawn liegt zwischen __1__-__2__ Chunks von der Mitte der Karte entfernt.\nDer ferne Spawn liegt zwischen __3__-__4__ Chunks von der Mitte der Karte entfernt.\nAlleine Spawnen ist gefährlich! Du wirst kämpfen müssen, um andere Spieler erreichen zu können. - -oarc-player-is-joining-main-force=__1__ tritt dem Standardteam bei! -oarc-player-is-joining-near=__1__ nimmt aus naher Entfernung am Spiel teil! -oarc-player-is-joining-far=__1__ nimmt aus großer Entfernung am Spiel teil! - -oarc-please-wait=BITTE WARTE, BIS DEIN SPAWN-PUNKT GENERIERT WURDE! - -oarc-looking-for-buddy=__1__ sucht nach einem Buddy. - -oarc-avail-bases-join=Verfügbare Basen zum Beitreten: -oarc-spawn-spots-remaining=__1__ (__2__ verbleibende Plätze) -oarc-cancel-return-to-previous=Abbrechen (zurück zu den vorherigen Optionen) -oarc-player-requesting-join-you=__1__ bittet darum, deiner Basis beizutreten! -oarc-waiting-for-spawn-owner=Warte auf die Antwort des Spawn-Besitzers... - -oarc-you-will-spawn-once-host=Du wirst spawnen, sobald der Host ja gewählt hat... -oarc-player-cancel-join-request=__1__ hat die Anfrage zum Beitritt zu deinem Spawn abgebrochen. - -oarc-spawn-ctrl=Spawn-Einst. -oarc-spawn-controls=Spawn Einstellungen: -oarc-spawn-allow-joiners=Erlaube anderen, deiner Basis beizutreten. - -oarc-set-respawn-loc=Setze neuen Spawn-Punkt (1 Stunde Abklingzeit) -oarc-set-respawn-loc-cooldown=Set Respawn Cooldown Remaining: __1__ -oarc-set-respawn-note=Das setzt deinen Spawn-Punkt an deine aktuelle Position. - -oarc-select-player-join-queue=Wähle einen Spieler aus der Beitrittswarteschlange aus: - -oarc-accept=Akzeptieren -oarc-reject=Ablehnen - -oarc-no-player-join-reqs=Du hast keine Anfragen von Spielern, deiner Base beizutreten. - -oarc-start-shared-base=Neue Spieler können nun __1__'s Base beitreten! -oarc-stop-shared-base=Neue Spieler können nicht länger der Base von __1__ beitreten! - -oarc-spawn-point-updated=Re-Spawn Punkt aktualisiert! -oarc-selected-player-not-wait=Der ausgewählte Spieler wartet nicht mehr auf den Beitritt! -oarc-reject-joiner=Du lehnst __1__'s Anfrage, deiner Base beizutreten, ab. -oarc-your-request-rejected=Deine Anfrage zum Beitritt wurde abgelehnt. - -oarc-player-joining-base=__1__ trat der Base von __2__ bei! - -oarc-player-left-while-joining=__1__ verließ das Spiel. Was für ein Arsch. - -oarc-buddy-spawn-options=Buddy Spawn Optionen -oarc-buddy-spawn-instructions=Um dies zu nutzen, vergewissere dich, dass du und dein Buddy zur gleichen Zeit in diesem Menü sind. Nur einer von euch muss die Anfrage senden. Wähle deinen Buddy aus der Liste aus (aktualisiere, wenn der Name deines Buddys nicht sichtbar ist) und wähle deine Spawn-Optionen. Klicke auf eine der Schaltflächen für die Anforderung, um die Anforderung zu senden. Der andere Buddy kann dann die Anfrage annehmen (oder ablehnen). Dies ermöglicht es euch beiden, nebeneinander zu spawnen, jeder mit seinem eigenen Spawn-Bereich. Sobald ein Buddy eine Spawn-Anfrage annimmt, ist sie endgültig! - -oarc-buddy-select-info=Wähle zunächst einen Buddy aus der Warteliste aus. Wähle dann die Spawn-Optionen aus und sende deine Anfrage: - -oarc-buddy-refresh=Buddy-Liste aktualisieren -oarc-create-buddy-team=Erstelle dein eigenes Team (geteilte Entwicklung) - -oarc-buddy-spawn-near=Erfrage Buddy-Spawn (nah) -oarc-buddy-spawn-far=Erfrage Buddy-Spawn (fern) - -oarc-invalid-buddy=Du hast keinen gültigen Buddy ausgewählt, versuche es noch mal. -oarc-buddy-not-avail=Der ausgewählte Buddy ist nicht länger vefügbar, bitte versuche es erneut. - -oarc-waiting-for-buddy=Warte auf die Antwort vom Buddy... -oarc-wait-buddy-select-yes=Du wirst spawnen, sobald dein Buddy ja gewählt hat... - -oarc-buddy-cancel-request=__1__ brach die Buddy-Anfrage ab! - -oarc-buddy-requesting-from-you=__1__ fordert einen Buddy Spawn von dir an! - -oarc-buddy-txt-main-team=Das Standardteam -oarc-buddy-txt-new-teams=in getrennten Teams -oarc-buddy-txt-buddy-team=ein Buddy Team -oarc-buddy-txt-moat= umgeben von einem Graben -oarc-buddy-txt-near=in der Nähe der Mitte der Karte! -oarc-buddy-txt-far=weit weg von der Mitte der Karte! -oarc-buddy-txt-would-like= möchte teilnehmen -oarc-buddy-txt-next-to-you= neben dir - -oarc-buddy-declined=__1__ hat deine Buddy-Anfrage abgelehnt! - -oarc-spawn-wait=Bitte warte! -oarc-wait-text=Dein Spawn wird gerade erstellt. Du wirst in __1__ Sekunden dorthin teleportiert!\nHalte dich bereit... - - - - - - - - - diff --git a/locale/en/locale-mod-settings.cfg b/locale/en/locale-mod-settings.cfg new file mode 100644 index 0000000..259dbee --- /dev/null +++ b/locale/en/locale-mod-settings.cfg @@ -0,0 +1,122 @@ +[mod-description] +oarc-mod=This is a multiplayer mod that allows every player to create their own spawn point when they join the game. There are a lot of helpful features to ensure that new players can join at anytime in the game or even join other player's spawn areas.\n\n[color=red][font=default-bold]Please check out the github page and discord for more information and support.[/font][/color]\n\n[font=default-small]This USED to be available as a scenario, but now is only provided as a mod. To start a new game with this mod, just use the default freeplay scenario. The scenario included in this mod only provides a template to overwrite the default freeplay scenario. It also provides a way for experienced server hosts to configure settings from a file instead of through the usual mod settings. Please read the control.lua file inside the scenario for notes.[/font] + +[mod-setting-name] +oarc-mod-default-allow-spawning-on-other-surfaces=Default to allow spawns on other surfaces +oarc-mod-linked-chest-size=Sharing chest capacity + +oarc-mod-welcome-msg-title=Welcome message title +oarc-mod-welcome-msg=Welcome message +oarc-mod-discord-invite=Discord Invite + +oarc-mod-enable-main-team=Enable main team +oarc-mod-enable-separate-teams=Enable separate teams + +oarc-mod-allow-moats-around-spawns=Allow moats around spawns +oarc-mod-enable-moat-bridging=Moat bridges +oarc-mod-minimum-distance-to-existing-chunks=Minimum distance to existing chunks +oarc-mod-near-spawn-distance=Spawn minimum distance +oarc-mod-far-spawn-distance=Spawn maximum distance + +oarc-mod-enable-buddy-spawn=Enable buddy spawn +oarc-mod-enable-offline-protection=Offline protection +oarc-mod-enable-shared-team-vision=Shared team vision +oarc-mod-enable-shared-team-chat=Shared team chat +oarc-mod-enable-shared-spawns=Shared spawns +oarc-mod-number-of-players-per-shared-spawn=Number of players per shared spawn +oarc-mod-enable-friendly-fire=Enable friendly fire + +oarc-mod-main-force-name=Main force name +oarc-mod-default-surface=Default starting surface +oarc-mod-enable-secondary-spawns=Enable secondary spawns + +oarc-mod-scale-resources-around-spawns=Scale resources around spawns +oarc-mod-modified-enemy-spawning=Scale enemies around spawns +oarc-mod-modified-enemy-easy-evo=Easy enemy evolution +oarc-mod-modified-enemy-medium-evo=Medium enemy evolution +oarc-mod-minimum-online-time=Minimum online time +oarc-mod-respawn-cooldown-min=Reset respawn point cooldown +oarc-mod-enable-shared-power=Shared power +oarc-mod-enable-shared-chest=Shared chest + +oarc-mod-enable-regrowth=Regrowth (map cleanup) +oarc-mod-enable-world-eater=World eater (map cleanup - additional) +oarc-mod-enable-abandoned-base-cleanup=Cleanup abandoned bases +oarc-mod-regrowth-cleanup-interval-min=Regrowth cleanup interval + +oarc-mod-spawn-general-radius-tiles=Spawn area radius +oarc-mod-spawn-general-moat-width-tiles=Spawn moat width +oarc-mod-spawn-general-tree-width-tiles=Spawn tree ring width +oarc-mod-spawn-general-enable-resources-circle-shape=Spawn resource deposits shape +oarc-mod-spawn-general-enable-force-grass=Force spawn area grass +oarc-mod-spawn-general-shape=Spawn area shape + +oarc-mod-resource-placement-enabled=Starting resource auto placement +oarc-mod-resource-placement-distance-to-edge=Starting resource distance to edge +oarc-mod-resource-placement-angle-offset=Starting resource angle offset +oarc-mod-resource-placement-angle-final=Starting resource angle final +oarc-mod-resource-placement-vertical-offset=Starting resource vertical offset +oarc-mod-resource-placement-horizontal-offset=Starting resource horizontal offset +oarc-mod-resource-placement-linear-spacing=Starting resource linear spacing +oarc-mod-resource-placement-size-multiplier=Starting resource size multiplier +oarc-mod-resource-placement-amount-multiplier=Starting resource amount multiplier + +[mod-setting-description] +oarc-mod-default-allow-spawning-on-other-surfaces=This controls the default starting setting for whether to allow spawning on other surfaces. If enabled, by default all other surfaces will be available for players to spawn on. [color=red]If you have other mods installed that add additional surfaces, I recommend leaving this disabled. Regardless of this setting, you can configure which surfaces allow spawning using the in game settings menu.[/color] +oarc-mod-linked-chest-size=This is the size of the shared chest that players can use to share items with other players. This is only meaningful if the shared chest feature is enabled. + +oarc-mod-welcome-msg-title=This is the title of the welcome message that will be displayed to players when they join the game. +oarc-mod-welcome-msg=This is the welcome message that will be displayed to players when they join the game and in the info panel. [color=red]Leave a single space to disable.[/color] +oarc-mod-discord-invite=Place your discord invite here so players can easily copy it from the in game info panel. [color=red]Leave a single space to disable.[/color] + +oarc-mod-enable-main-team=Allow players to join the main team. This is the default team that is created when the game starts. This lets players share research progress.\n[color=red][font=default-bold]You must enable one or both of the main team and separate team options. Otherwise it will default to main team allowed only.[/font][/color] +oarc-mod-enable-separate-teams=Allow players to start their own teams (CO-OP only, No PVP). This lets players have their own research progress.\n[color=red][font=default-bold]You must enable one or both of the main team and separate team options. Otherwise it will default to main team allowed only.[/font][/color] + +oarc-mod-allow-moats-around-spawns=Allow players to choose spawns with a moat around them. +oarc-mod-enable-moat-bridging=If the spawn area is surrounded by land, the moat will have a small land bridge connecting it. +oarc-mod-minimum-distance-to-existing-chunks=This is the minimum distance a new spawn will be created from existing chunks (even if not visible). +oarc-mod-near-spawn-distance=The minimum distance a player can choose to spawn from the origin. This is used as a starting point for the search algorithm so is not a guaranteed distance. +oarc-mod-far-spawn-distance=The maximum distance a player can choose to spawn from the origin. This is used as a starting point for the search algorithm so is not a guaranteed distance. + +oarc-mod-enable-buddy-spawn=Allow spawning with a buddy. 2 players can spawn next to each other with their own starting areas. +oarc-mod-enable-offline-protection=This inhibits enemy attacks on bases where all players are offline. Not 100% guaranteed! +oarc-mod-enable-shared-team-vision=Makes sure all teams can see each other's map and radar coverage. +oarc-mod-enable-shared-team-chat=All teams can see each other's chat messages. +oarc-mod-enable-shared-spawns=Allow players to share their spawn areas for other players to join. +oarc-mod-number-of-players-per-shared-spawn=Number of players that can join a shared spawn, including the original player. +oarc-mod-enable-friendly-fire=Enables friendly fire. So you can shoot your chests (or run over your friends with a train). This lets you damage your OWN team. + +oarc-mod-main-force-name=The name of the main force. This is the default team that is created when the game starts. +oarc-mod-default-surface=The default surface that players will spawn on if they join the main team or if spawning on other surfaces is not enabled. +oarc-mod-enable-secondary-spawns=Enabling this will provide players with a secondary spawn point when they first move to a new surface/planet. This is only applicable if the other surface is enabled for separate spawns. + +oarc-mod-scale-resources-around-spawns=This scales resources around every spawn area so far away spawns aren't immediately next to very rich deposits. +oarc-mod-modified-enemy-spawning=This scales the enemy spawning globally based on the allowed spawn distances to avoid every spawn being surrounded by behemoth worms. +oarc-mod-modified-enemy-easy-evo=This is the maximum evolution that the enemies in the warning zone (closest) to a spawn will be FIXED at to ensure all players have a safe start! The warning zone area can be configured via the custom scenario. +oarc-mod-modified-enemy-medium-evo=This is the maximum evolution that the enemies in the danger zone (next closest) to a spawn will be allowed to reach. Helps ensure players have a safe start! The danger zone area can be configured via the custom scenario. +oarc-mod-minimum-online-time=The minimum time a player must be online before they leave, otherwise their spawn area will be cleaned up. +oarc-mod-respawn-cooldown-min=The minimum time a player must wait before they can change their spawn point to a new location. +oarc-mod-enable-shared-power=This allows players to share their power network with other players. It will create a special power pole at their spawn point that connects to the shared power network wirelessly. +oarc-mod-enable-shared-chest=This gives players a special chest they can use to share items with other players. It uses the "linked-chest" item that is a hidden feature in factorio. + +oarc-mod-enable-regrowth=Enables regrowth. This slowly removes inactive chunks over time. Any chunk with player structures will not be removed. This helps to keep the map (and file size) smaller over time. +oarc-mod-enable-world-eater=Enables world eater. This requires regrowth. This slowly checks all chunks for a lack of player structures and entities and marks them for removal. This helps to keep the map (and file size) smaller over time. This is more of an experimental feature, use at your own risk. +oarc-mod-enable-abandoned-base-cleanup=Abandoned bases will be cleaned up if players leave within a short time of joining. [color=red]This does NOT require regrowth or world eater to be enabled.[/color] +oarc-mod-regrowth-cleanup-interval-min=This is the interval in minutes that the regrowth cleanup will run. Whenever a map cleanup happens, the game freezes for a second, so you don't want to run this too often! Abandoned base cleanup happens immediately when a player leaves and is not affected by this setting. + +oarc-mod-spawn-general-radius-tiles=This is the radius of the spawn area in tiles. I would not make this much smaller then 2 chunks (64) and not too huge unless you want spawns to take a long time to generate. +oarc-mod-spawn-general-moat-width-tiles=This is the width of the moat around the spawn area in tiles, if a moat is enabled and selected at spawn time. +oarc-mod-spawn-general-tree-width-tiles=This is the width of the tree ring around the spawn area in tiles. It guarantees some trees near to spawn. +oarc-mod-spawn-general-enable-resources-circle-shape=This is the shape of the starting area resource deposits. +oarc-mod-spawn-general-enable-force-grass=Enabling this will make the entire spawn area pure grass. Disabling will use landfill as needed instead. +oarc-mod-spawn-general-shape=This is the shape of the spawn area. + +oarc-mod-resource-placement-enabled=You should leave this enabled unless you are manually specifying resource placements in the custom scenario! +oarc-mod-resource-placement-distance-to-edge=This is the distance from the edge of the spawn area that resources will be placed. Only applicable for circle/octagon shaped spawns. +oarc-mod-resource-placement-angle-offset=This is the starting angle offset (in radians) for the resource placement. At what angle (in radians) do resources start. 0 = east. 3.14 = west. Resources are placed clockwise starting at this angle. Only applicable for circle/octagon shaped spawns. +oarc-mod-resource-placement-angle-final=This is the final angle offset (in radians) for the resource placement. At what angle (in radians) do resources end. 0 = east. 3.14 = west. Resources are placed clockwise ending at this angle. Only applicable for circle/octagon shaped spawns. +oarc-mod-resource-placement-vertical-offset=This is the vertical offset (in tiles) for the resource placement from the top-left of the spawn. Only applicable for square shaped spawns. +oarc-mod-resource-placement-horizontal-offset=This is the horizontal offset (in tiles) for the resource placement from the top-left of the spawn. Only applicable for square shaped spawns. +oarc-mod-resource-placement-linear-spacing=This is the linear spacing (in tiles) between resources. Only applicable for square shaped spawns. +oarc-mod-resource-placement-size-multiplier=This changes the size of the resource deposits. Default settings should provide close to a vanilla starting experience. +oarc-mod-resource-placement-amount-multiplier=This changes the richness of the resource deposits. Default settings should provide close to a vanilla starting experience. \ No newline at end of file diff --git a/locale/en/locale.cfg b/locale/en/locale.cfg index ffe47af..0335831 100644 --- a/locale/en/locale.cfg +++ b/locale/en/locale.cfg @@ -1,53 +1,86 @@ -scenario-name=Oarc's Multiplayer Spawning Scenario +scenario-name=OARC -description=This scenario allows every player to create their own spawn point when they join the game. There are a lot of helpful features to ensure that new players can join at anytime in the game or even join other players. This is a multiplayer only scenario.\n[color=red][font=default-bold]You must first create a config.lua file if one does not yet exist![/font][/color]\nIn the scenario folder, copy "example-config.lua" and name it "config.lua". Edit the file with any text editor. This is the only way to configure your scenario options and it MUST be done before starting a new game.\n[color=yellow][font=default-bold]Always start a new game when changing the config or updating the scenario![/font][/color]\nPlease visit https://github.com/Oarcinae/FactorioScenarioMultiplayerSpawn for the scenario source code, Wiki, and if you want to file any bugs. The wiki has instructions on how to best start games on dedicated/headless servers!\n[color=yellow][font=default-bold]If you got this scenario from the mod portal or by joining a server, make sure you remove any blueprint.zip files in the scenario folder![/font][/color] +description=This mod provides a scenario that overhauls multiplayer spawning.\n[color=red][font=default-bold]Please check out the github page and discord for more information and support.[/font][/color] -oarc-spawn-time-warning-msg=Due to the way this scenario works, it may take some time for the land around your new spawn area to generate... Please wait for 10-20 seconds when you select your first spawn. +oarc-scenario-info-warn-msg=This scenario allows players to create their own spawn points far from the center of the map. For more information, click the OARC button in the top left corner. oarc-i-understand=I Understand oarc-spawn-options=Spawn Options -oarc-click-info-btn-help=Click the INFO button in the top left to learn more about this scenario! This is your ONLY chance to choose a spawn option. Choose carefully... +oarc-click-info-btn-help=Click the OARC button in the top left to learn more about this scenario! This is your ONLY chance to choose a spawn option. Choose carefully... +oarc-gui-tooltip=Click to open and close the mod gui. This provides settings and information about the mod. oarc-vanilla-spawn=Vanilla Spawn oarc-default-spawn-behavior=This is the default spawn behavior of a vanilla game. You join the default team in the center of the map. +oarc-surface-select-tooltip=Select the surface you would like to spawn on. (Disabled if only one surface is available.) + oarc-join-main-team-radio=Join Main Team (shared research) +oarc-join-main-team-tooltip=You will share research progress with other players on the main team. + oarc-create-own-team-radio=Create Your Own Team (own research tree) +oarc-create-own-team-tooltip=You will have your own research tree and will not share research with players on the main team. oarc-moat-option=Surround your spawn with a moat +oarc-moat-option-tooltip=If you spawn near land, a ring of water will be generated around your spawn area. Depending on the mod options, there may be a small land bridge created as well. If you spawn in water already, this option has no effect. -oarc-solo-spawn-near=Solo Spawn (Near) -oarc-solo-spawn-far=Solo Spawn (Far) +oarc-solo-spawn=Create Your Own Spawn +oarc-solo-spawn-tooltip=You will spawn in your own starting area with pre-set starting resources. oarc-starting-area-vanilla=You are spawned in your own starting area (vanilla style). oarc-vanilla-spawns-available=There are __1__ vanilla spawns available. +oarc-spawn-menu-settings-info=These settings apply when creating a new spawn. They do not have any effect if you are joining a shared spawn. +oarc-spawn-menu-settings-header=Spawn Settings +oarc-spawn-menu-solo-header=Create Your Own Spawn +oarc-spawn-menu-shared-header=Join Another Spawn +oarc-spawn-menu-buddy-header=Create A Buddy Spawn + +oarc-spawn-distance-slider-label=Distance From Map Center: +oarc-spawn-distance-slider-tooltip=This is the minimum distance from the center of the map in chunks that you will spawn. You may spawn further away if the map has already been explored. Resources and enemies are both more abundant the further you go from the center, but you will still be guaranteed a safe starting area. + oarc-starting-area-normal=You are spawned in a new area, with pre-set starting resources. -oarc-join-someone-avail=Join Someone (__1__ available) -oarc-join-someone-info=You are spawned in someone else's base. This requires at least 1 person to have allowed access to their base. This choice is final and you will not be able to create your own spawn later. +oarc-join-someone-dropdown-label=Select Shared Spawn: +oarc-join-someone-dropdown-tooltip=Select a player to REQUEST to join their shared spawn area. +oarc-join-shared-button-enable=Join (__1__ on __2__) +oarc-join-shared-button-disable=Select A Player First! +oarc-join-shared-button-tooltip=This will send a request to join the selected player's shared spawn area. You can cancel the request if you change your mind. +oarc-join-someone-info=You can request to join someone else's starting area. This requires at least 1 person to have allowed access to their base and to have space for you to join. oarc-no-shared-avail=There are currently no shared bases availble to spawn at. oarc-join-check-again=Check Again oarc-shared-spawn-disabled=Shared spawns are disabled in this mode. +oarc-no-shared-spawn-selected=You have not selected a valid player to join. Please select one from the list. +oarc-invalid-host-shared-spawn=Selected player is no longer available! Please try again. -oarc-buddy-spawn=Buddy Spawn +oarc-buddy-spawn=Request Buddy Spawn +oarc-buddy-spawn-tooltip=This will send the request to your selected buddy. They may choose to accept or deny the request. You may also cancel the request if you change your mind. +oarc-buddy-spawn-disabled=Buddy spawns are disabled in this mode. oarc-buddy-spawn-info=The buddy system requires 2 players in this menu at the same time, you spawn beside each other, each with your own resources. -oarc-max-players-shared-spawn=If you create your own spawn point you can allow up to __1__ other online players to join. -oarc-spawn-dist-notes=Near spawn is between __1__-__2__ chunks away from the center of the map.\nFar spawn is between __3__-__4__ chunks away from the center of the map.\nSolo spawns are dangerous! Expect a fight to reach other players. +oarc-max-players-shared-spawn=You can allow up to __1__ other players to join you. +oarc-spawn-dist-notes=Spawn distance is in CHUNKS from the center of the map.\nExpect a fight to reach other players. -oarc-player-is-joining-main-force=__1__ is joining the main force! -oarc-player-is-joining-near=__1__ is joining the game from a distance! -oarc-player-is-joining-far=__1__ is joining the game from a great distance! +oarc-player-is-joining=__1__ is joining the game on __2__! +oarc-player-started-own-team=__1__ has started their own team! +oarc-player-no-new-teams-sorry=Sorry, no new teams can be created. You were assigned to the default team instead. +oarc-no-ungenerated-land-error=[color=red]Failed to find ungenerated land to spawn on! Please try again. If you see this message multiple times, it could be a settings or mod conflict issue.[/color] +oarc-buddies-are-joining=__1__ and __2__ are joining the game together on __3__! +oarc-player-new-secondary=__1__ has started a secondary base on __2__! oarc-please-wait=PLEASE WAIT WHILE YOUR SPAWN POINT IS GENERATED! +oarc-player-left-early=Player (__1__) left the game before __2__ minutes of play time. Their spawn area has been marked for cleanup. +oarc-generating-spawn-please-wait=Generating your spawn now, please wait... +oarc-host-left-new-host=__1__ has left so __2__ now owns their base. +oarc-new-owner-msg=You have been given ownership of this base! + oarc-looking-for-buddy=__1__ is looking for a buddy. oarc-avail-bases-join=Available Bases to Join: oarc-spawn-spots-remaining=__1__ (__2__ spots remaining) -oarc-cancel-return-to-previous=Cancel (Return to Previous Options) +oarc-cancel-button-caption=Cancel +oarc-return-to-previous-tooltip=Return to the previous menu. oarc-player-requesting-join-you=__1__ is requesting to join your base! oarc-waiting-for-spawn-owner=Waiting for spawn owner to respond... @@ -55,18 +88,39 @@ oarc-you-will-spawn-once-host=You will spawn once the host selects yes... oarc-player-cancel-join-request=__1__ cancelled their request to join your spawn. oarc-spawn-ctrl=Spawn Ctrl -oarc-spawn-controls=Spawn Controls: -oarc-spawn-allow-joiners=Allow others to join your base. +oarc-primary-spawn-info-header=Primary Spawn Information +oarc-primary-spawn-info-note=This is your home base location. If you let other players join your base, they will spawn here. +oarc-primary-spawn-info-surface-label=Home: __1__ (x=__2__, y=__3__) + +oarc-secondary-spawn-info-header=Secondary Spawn Information +oarc-secondary-spawn-info-note=These are your secondary base locations. +oarc-secondary-spawn-info-surface-label=Secondary: __1__ (x=__2__, y=__3__) + +oarc-spawn-info-location-button=Show Map Location +oarc-spawn-info-location-button-tooltip=Click to show the location on the map. + -oarc-set-respawn-loc=Set New Respawn Location (has a cooldown) +oarc-spawn-gps-location=Location: +oarc-shared-spawn-controls=Shared Spawn Controls +oarc-shared-spawn-allow-joiners=Allow others to join your base. +oarc-shared-spawn-full=Your base is full! No more players can join. + +oarc-set-respawn-loc-header=Respawn Location +oarc-set-respawn-loc-info-surface-label=Current Surface Respawn: __1__ (x=__2__, y=__3__) +oarc-set-respawn-loc=Set Respawn Location +oarc-set-respawn-loc-tooltip=This will set your respawn point to your current location. There is a cooldown before you can change it again. oarc-set-respawn-loc-cooldown=Set Respawn Cooldown Remaining: __1__ -oarc-set-respawn-note=This will set your respawn point to your current location. +oarc-set-respawn-note=This will set your respawn point to your current location. Change tabs to update the timer. +oarc-join-queue-header=Shared Spawn Queue oarc-select-player-join-queue=Select a player from the join queue: oarc-accept=Accept oarc-reject=Reject +oarc-enabled=enabled +oarc-disabled=disabled + oarc-no-player-join-reqs=You have no players requesting to join you at this time. oarc-start-shared-base=New players can now join __1__'s base! @@ -74,20 +128,24 @@ oarc-stop-shared-base=New players can no longer join __1__'s base! oarc-spawn-point-updated=Re-spawn point updated! oarc-selected-player-not-wait=Selected player is no longer waiting to join! +oarc-selected-player-not-valid=Please select a valid player! oarc-reject-joiner=You rejected __1__'s request to join your base. oarc-your-request-rejected=Your request to join was rejected. oarc-player-joining-base=__1__ is joining __2__'s base! -oarc-player-left-while-joining=__1__ left the game. What an ass. - +oarc-buddy-select-label=Select Buddy: +oarc-buddy-select-tooltip=Select a buddy from the list to request a buddy spawn. oarc-buddy-spawn-options=Buddy Spawn Options -oarc-buddy-spawn-instructions=To use this, make sure you and your buddy are in this menu at the same time. Only one of you must send the request. Select your buddy from the list (refresh if your buddy's name is not visible) and select your spawn options. Click one of the request buttons to send the request. The other buddy can then accept (or deny) the request. This will allow you both to spawn next to each other, each with your own spawn area. Once a buddy accepts a spawn request, it is final! +oarc-buddy-spawn-instructions=You and your buddy must be in this menu at the same time. Only one of you can send the request. Select your your spawn options first, then select your buddy from the list. Click Request Buddy Spawn. The other buddy can then accept (or deny) the request. This will allow you both to spawn next to each other, each with your own spawn area. oarc-buddy-select-info=First, select a buddy from the waiting list. Then choose the spawn options and send your request: +oarc-buddy-spawn-request-header=Buddy Request! + oarc-buddy-refresh=Refresh Buddy List oarc-create-buddy-team=Create Your Own Buddy Team (buddy and you share research) +oarc-create-buddy-team-tooltip=If this is enabled, you and your buddy will share research progress. oarc-buddy-spawn-near=Request Buddy Spawn (Near) oarc-buddy-spawn-far=Request Buddy Spawn (Far) @@ -102,16 +160,100 @@ oarc-buddy-cancel-request=__1__ cancelled their buddy request! oarc-buddy-requesting-from-you=__1__ is requesting a buddy spawn from you! +# [BUDDY NAME] oarc-buddy-txt-would-like {team choice} oarc-buddy-txt-next-to-you {moat choice} {surface choice} {distance choice} +oarc-buddy-txt-would-like= would like to join oarc-buddy-txt-main-team=the main team oarc-buddy-txt-new-teams=on separate teams oarc-buddy-txt-buddy-team=a buddy team -oarc-buddy-txt-moat= surrounded by a moat -oarc-buddy-txt-near=near to the center of the map! -oarc-buddy-txt-far=far from the center of the map! -oarc-buddy-txt-would-like= would like to join oarc-buddy-txt-next-to-you= next to you +oarc-buddy-txt-moat= surrounded by a moat +oarc-buddy-txt-surface= on __1__ +oarc-buddy-txt-distance= at a distance of __1__ chunks from the center of the map! + oarc-buddy-declined=__1__ declined your buddy request! oarc-spawn-wait=Please wait! -oarc-wait-text=Your spawn is being created now.\nYou will be teleported there in __1__ seconds!\nPlease standby... \ No newline at end of file +oarc-wait-text=Your spawn is being created now.\nYou will be teleported there in __1__ seconds!\nPlease standby... + +oarc-gui-tab-header-label=Scenario Info and Controls +oarc-server-info-tab-title=Server Info +oarc-spawn-ctrls-tab-title=Spawn Controls +oarc-settings-tab-title=Settings +oarc-mod-info-tab-title=Mod Info + +oarc-server-info-tab-welcome-msg-title=Welcome Message +oarc-server-info-tab-discord-invite=Discord Invite: +oarc-server-info-tab-discord-invite-tooltip=Come join the discord (copy this invite)! +oarc-server-info-tab-map-info-label=Map Info +oarc-server-info-tab-server-run-time=Server Run Time: __1__ +oarc-server-info-leave-warning=If you leave within __1__ minutes of joining, your base and character will be deleted. +oarc-server-info-admin-controls=Admin Controls +oarc-server-info-ban-select-player=Select Player: +oarc-server-info-button-ban-player=Ban Player +oarc-server-info-button-restart-player=Restart Player + +oarc-player-not-found=Player __1__ is not found? +oarc-player-about-to-spawn=Player __1__ is about to spawn, try again later. +oarc-player-none-selected=No player selected! + + +oarc-settings-tab-title-mod-settings=Mod Settings +oarc-settings-tab-admin-warning=You are an admin. Changing these settings late in the game may cause issues!\nChanging settings will not modify existing spawns. BE CAREFUL! +oarc-settings-tab-player-warning=You are not an admin. These settings are read-only for you. +oarc-settings-tab-description=This tab contains the same mod settings in the mod settings menu AND some additional settings that can only be changed in game. +oarc-settings-tab-text-field-enter-tooltip=[color=red]You must press ENTER after typing in a text field to save the value![/color] +oarc-settings-tab-title-surface=Surface Settings +oarc-settings-tab-surface-checkbox-tooltip=Enabling this will allow custom spawn areas (the main feature of this mod) on this surface. You need at least one of these enabled for the mod to work. +oarc-settings-tab-surface-regrowth-checkbox-tooltip=Enabling this will allow the regrowth and world eater features to work on this surface, if those are enabled. +oarc-settings-tab-surface-column-header=Surface +oarc-settings-tab-surface-spawning-enabled=Custom Spawns +oarc-settings-tab-surface-regrowth-enabled=Regrowth + + +oarc-settings-section-header-server-info=Server Info +oarc-settings-section-header-gameplay=Gameplay +oarc-settings-section-header-regrowth=Regrowth +oarc-settings-section-header-general-spawn=General Spawn Area Config +oarc-settings-section-header-resource-placement=Starting Resources Placement + +oarc-settings-section-subheader-spawn-choices=Spawn choices +oarc-settings-section-subheader-difficulty-scaling=Enemy and resource scaling +oarc-settings-section-subheader-gameplay-misc=Misc +oarc-settings-section-subheader-sharing=Sharing and co-op + +oarc-settings-section-subheader-resource-placement-circular=For circle/octagon spawns +oarc-settings-section-subheader-resource-placement-square=For square spawns + +oarc-settings-section-subheader-regrowth-warning=These features can help reduce save file size but can be more UPS intensive.\n + +oarc-shared-power-pole-helper-txt=This connects to a shared electric network for all players. +oarc-shared-chest-helper-txt=This connects to a shared storage chest for all players. + +oarc-mod-faq-what-is-this-mod=What is this mod? +oarc-mod-faq-what-is-this-mod-answer=This mod overhauls multiplayer spawning. Players can create their own spawn points away from the center of the map. Players can also join other players' bases, or create buddy spawns. The mod does not change the core gameplay of Factorio, but it does change the way players start the game. The starting area is a preset area with resources, not a "natural" spawn point. +oarc-mod-faq-other-surfaces=Can I start on a different surface/planet? +oarc-mod-faq-other-surfaces-answer=[color=red]This feature is currently a work in progress. As such, only spawning on "nauvis" or a default nauvis-like surface will work correctly.[/color] +oarc-mod-faq-secondary-spawns=What are secondary spawns? +oarc-mod-faq-secondary-spawns-answer=If enabled, when a player first travels to a new surface that has custom spawning enabled, they will automatically be given a new starting area based on their primary spawn choice (the first spawn they create). [color=red]This feature is currently a work in progress. Secondary spawning on other surfaces may cause issues. Buddy and shared spawns are not implemented yet at this time.[/color] +oarc-mod-faq-what-are-teams=What are the different team options? +oarc-mod-faq-what-are-teams-answer=I wrote this mod for co-op play only, not pvp. Depending on the mod settings, you can either join the main team, which shares research, or create your own team, which has its own research tree. All teams/forces are friendly to each other and can communicate in chat and (optionally) share map vision. The only reason to create your own team is if you want to have your own research progress. +oarc-mod-faq-shared-spawn=What is a shared spawn? +oarc-mod-faq-shared-spawn-answer=Players can choose to allow other players to join their base. This allows new players to spawn in the same starting area as the host player. The host player can control who can join their base. +oarc-mod-faq-buddy-spawn=What is a buddy spawn? +oarc-mod-faq-buddy-spawn-answer=If you and another player are in the spawn menu at the same time, you can request a buddy spawn. This will spawn you and your buddy right next to each other on the map, each with your own starting area. +oarc-mod-faq-regrowth=What are the regrowth and world eater features? +oarc-mod-faq-regrowth-answer="Regrowth" helps keep the save file size down by removing inactive map chunks over time. This is useful for long-running servers with many players. No chunks with player activity will be removed. "World Eater" slowly checks every chunk to see if there are any player structures in it. This is an additional check on top of the regrowth feature that will catch any chunks that players built on, but then later removed all structures from. +oarc-mod-faq-cleanup-abandoned=What is the cleanup abandoned bases feature? +oarc-mod-faq-cleanup-abandoned-answer=This removes the spawn area if the player leaves within the "minimum online time" and will also delete their character. This feature is useful for keeping the map clean and the save file size down. Public servers likely want to enable this feature. +oarc-mod-faq-offline-protection=What is the offline protection feature? +oarc-mod-faq-offline-protection-answer=If a base has all players offline, it will be protected from enemy attacks. This is not 100% guaranteed, but it will help prevent the base from being destroyed while players are offline. +oarc-mod-faq-shared-power=What is the shared power feature? +oarc-mod-faq-shared-power-answer=This allows players to share their power network with other players. It will create a special power pole at their spawn point that connects to the shared power network wirelessly. It utilizes cross-surface power poles to make "wireless" connections. +oarc-mod-faq-shared-chest=What is the shared chest feature? +oarc-mod-faq-shared-chest-answer=This gives players a special chest at their spawn area that they can use to share items with other players. It uses the "linked-chest" item that is a hidden feature in Factorio. This allows players to share items with other players in a secure way. + + +[entity-name] +oarc-linked-chest=OARC Linked Chest +oarc-linked-power=OARC Linked Power \ No newline at end of file diff --git a/locale/fr/locale.cfg b/locale/fr/locale.cfg deleted file mode 100644 index a044c87..0000000 --- a/locale/fr/locale.cfg +++ /dev/null @@ -1,113 +0,0 @@ -oarc-spawn-time-warning-msg=En raison de la manière de fonctionnement de ce scénario, la génération de votre base peut prendre un certain temps ... Veuillez patienter 10 à 20 secondes lorsque vous apparaissez. - -oarc-i-understand=Je comprends -oarc-spawn-options=Options d'apparition - -oarc-click-info-btn-help=Cliquez sur le bouton INFO en haut à gauche pour en savoir plus sur ce scénario! Ceci est votre SEULE chance de choisir une option de spawn. Choisissez soigneusement... - -oarc-vanilla-spawn=Spawn vanillia -oarc-default-spawn-behavior=C’est le comportement par défaut du spawn d’un jeu vanillia. Vous rejoignez l'équipe par défaut au centre de la carte. - -oarc-join-main-team-radio=Rejoindre l'équipe principale (recherche partagée) -oarc-create-own-team-radio=Créez votre propre équipe (propre arbre de recherche) - -oarc-moat-option=Entourez votre base d'eau (fossé) - -oarc-solo-spawn-near=Spawn solo (proche) -oarc-solo-spawn-far=Spawn solo (Loin) - -oarc-starting-area-vanilla=Vous êtes apparu dans votre propre base (style vanilla). -oarc-vanilla-spawns-available=Il y a __1__ une base vanilla disponible. - -oarc-starting-area-normal=Vous êtes apparu dans une nouvelle base, avec des ressources de départ prédéfinies. -oarc-join-someone-avail=Rejoindre quelqu'un (__1__ disponible) -oarc-join-someone-info=Vous rejoindrez la base de quelqu'un d'autre. Cela nécessite que au moins 1 personne autorise l'accès à leur base. Ce choix est définitif et vous ne pourrez pas créer votre propre zone plus tard. - -oarc-no-shared-avail=Il n’existe actuellement aucune base partagée disponible. -oarc-join-check-again=Vérifier à nouveau -oarc-shared-spawn-disabled=Les spawns partagés sont désactivés dans ce mode. - -oarc-buddy-spawn=Spawn avec un ami -oarc-buddy-spawn-info=Se système d'amis nécessite 2 joueurs dans ce menu en même temps, vous apparaissez côte à côte, chacun avec vos propres ressources. - -oarc-max-players-shared-spawn=Si vous créez votre propre point d'apparition, vous pouvez autoriser jusqu'à __1__ autres joueurs en ligne à rejoindre. -oarc-spawn-dist-notes=Le point d’apparition (proche) se situe entre __1__-__2__ morceaux du centre de la carte. \nLe point d’apparition (loin) se situe entre __3__-__4__ morceaux du centre de la carte. \nLes apparitions solo sont dangereuses! Attendez-vous à un combat pour atteindre d'autres joueurs. - -oarc-player-is-joining-main-force=__1__ rejoint l'équipe principale'! -oarc-player-is-joining-near=__1__ rejoint le jeu à distance! -oarc-player-is-joining-far=__1__ rejoint le jeu avec une grande distance! - -oarc-please-wait=VEUILLEZ ATTENDRE QUE VOTRE POINT D'APPARITION SOIT GÉNÉRÉ! - -oarc-looking-for-buddy=__1__ est à la recherche d'un copain. - -oarc-avail-bases-join=Bases disponibles à rejoindre: -oarc-spawn-spots-remaining=__1__ (__2__ places restantes) -oarc-cancel-return-to-previous=Annuler (retourner aux options précédentes) -oarc-player-requesting-join-you=__1__ demande à rejoindre votre base! -oarc-waiting-for-spawn-owner=Attendez que le propriétaire de la base réponde ... - -oarc-you-will-spawn-once-host=Vous apparaîtrez une fois que l'hôte aura choisi oui ... -oarc-player-cancel-join-request=__1__ a annulé sa demande pour rejoindre votre base. - -oarc-spawn-ctrl=Spawn Ctrl -oarc-spawn-controls=Contrôle d'apparition': -oarc-spawn-allow-joiners=Autoriser les autres à rejoindre votre base. - -oarc-set-respawn-loc=Définir le nouvel emplacement de réapparition (il y a une heure d'attente pour réutiliser ce boutton) -oarc-set-respawn-loc-cooldown=Définir le temps d'attente pour réapparaître restant: __1__ -oarc-set-respawn-note=Cela va définir votre point de réapparition à votre position actuelle. - -oarc-select-player-join-queue=Sélectionnez un joueur dans la file d'attente: - -oarc-accept=Accepter -oarc-reject=Rejeter - -oarc-no-player-join-reqs=Vous n'avez aucun joueur demandant à vous rejoindre pour le moment. - -oarc-start-shared-base=Les nouveaux joueurs peuvent maintenant rejoindre la base de __1__! -oarc-stop-shared-base=Les nouveaux joueurs ne peuvent plus rejoindre la base de __1__! - -oarc-spawn-point-updated=Point de réapparition mis à jour! -oarc-selected-player-not-wait=Le joueur sélectionné n'attend plus de rejoindre! -oarc-reject-joiner=Vous avez rejeté la demande de __1__ pour rejoindre votre base. -oarc-your-request-rejected=Votre demande d'adhésion a été rejetée. - -oarc-player-joining-base=__1__ à rejoint la base de __2__! - -oarc-player-left-while-joining=__1__ a quitté le match. Quel âne. - -oarc-buddy-spawn-options=Options d'apparition du menu (Spawn avec un ami) -oarc-buddy-spawn-instructions=Pour utiliser ceci, assurez-vous que votre ami et vous êtes dans ce menu en même temps. Un seul d'entre vous doit envoyer la demande. Sélectionnez votre ami dans la liste (actualisez-la si son nom n'est pas visible) et sélectionnez vos options d'apparition. Cliquez sur l'un des boutons de requête pour envoyer la requête. L'autre copain peut alors accepter (ou refuser) la demande. Cela vous permettra à tous les deux d'apparaître côte à côte, chacun avec votre propre base. Une fois que votre copain aura accepté une demande de spawn, c'est définitif! - -oarc-buddy-select-info=D'abord, sélectionnez un ami dans la liste d'attente. Puis choisissez les options de spawn et envoyez votre demande: - -oarc-buddy-refresh=Actualiser la liste d'amis -oarc-create-buddy-team=Créez votre propre équipe d'amis (vous et votre ami partagez la recherche) - -oarc-buddy-spawn-near=Demander la création de la base (proche) -oarc-buddy-spawn-far=Demander la création de la base (loin) - -oarc-invalid-buddy=Vous n'avez pas sélectionné de partenaire valide! Veuillez réessayer. -oarc-buddy-not-avail=Le copain sélectionné n'est plus disponible! Veuillez réessayer. - -oarc-waiting-for-buddy=Attendez que votre ami réponde ... -oarc-wait-buddy-select-yes=Vous apparaîtrez une fois que votre ami aura choisi oui ... - -oarc-buddy-cancel-request=__1__ a annulé sa demande! - -oarc-buddy-requesting-from-you=__1__ vous demande a appraître avec lui! - -oarc-buddy-txt-main-team=l'équipe principale -oarc-buddy-txt-new-teams=sur des équipes séparées -oarc-buddy-txt-buddy-team=une équipe de amis -oarc-buddy-txt-moat=entouré d'eau (fossé) -oarc-buddy-txt-near=proche du centre de la carte! -oarc-buddy-txt-far=loin du centre de la carte! -oarc-buddy-txt-would-like=aimerait rejoindre -oarc-buddy-txt-next-to-you=à côté de toi - -oarc-buddy-declined=__1__ a refusé votre demande d'ami! - -oarc-spawn-wait=Patienter s'il vous plaît! -oarc-wait-text=Votre spawn est en cours de création. \nVous serez téléporté dans __1__ secondes! \nVeuillez patienter ... diff --git a/locale/zh-CN/locale.cfg b/locale/zh-CN/locale.cfg deleted file mode 100644 index fbeacd0..0000000 --- a/locale/zh-CN/locale.cfg +++ /dev/null @@ -1,117 +0,0 @@ -scenario-name=Oarc多人游戏场景。 - -description=该场景允许每个玩家在加入游戏时创建自己的生成点。这里有许多便利功能,确保新玩家可以随时加入游戏或加入其他玩家的队伍。这是一个仅限多人游戏的场景。\n[color=red][font=default-bold]首先您必须创建一个config.lua文件,如果还没有的话![/font][/color]\n在场景文件夹内,复制"example-config.lua"并将其命名为"config.lua"。用任何文本编辑器编辑该文件。这是唯一配置场景选项的方法,必须在开始新游戏之前完成。\n[color=yellow][font=default-bold]更改配置或更新场景时,请始终开始新游戏![/font][/color]\n请访问https://github.com/Oarcinae/FactorioScenarioMultiplayerSpawn获取场景源代码、Wiki以及提交任何错误报告。Wiki上有关于如何在专用/无头服务器上最佳开始游戏的说明。\n[color=yellow][font=default-bold]如果你是从模组门户或通过加入服务器得到这个场景的,确保在场景文件夹中删除任何blueprint.zip文件![/font][/color] - -oarc-spawn-time-warning-msg=由于这个场景的运作方式,你新出生点周围的土地可能需要一些时间来生成...当你选择你的第一个生成点时,请等待10-20秒。 - -oarc-i-understand=我明白了 -oarc-spawn-options=生成选项 - -oarc-click-info-btn-help=点击左上角的信息按钮以了解更多关于这个场景的信息!这是你唯一选择生成选项的机会,请慎重选择... - -oarc-vanilla-spawn=原版生成 -oarc-default-spawn-behavior=这是原版游戏的默认生成行为。你将加入地图中心的默认队伍。 - -oarc-join-main-team-radio=加入主队(共享科技) -oarc-create-own-team-radio=创建你自己的队伍(独立科技树) - -oarc-moat-option=用护城河包围你的出生点 - -oarc-solo-spawn-near=单人出生点(近) -oarc-solo-spawn-far=单人出生点(远) - -oarc-starting-area-vanilla=你将在自己的起始区域生成(原版风格)。 -oarc-vanilla-spawns-available=目前有 __1__ 个原版生成点可用。 - -oarc-starting-area-normal=你将在一个新出生点出生。 -oarc-join-someone-avail=加入他人(有 __1__ 个可用) -oarc-join-someone-info=你将在别人的基地出生。这需要至少有1个人开放了他们的基地。这个选择是最终的,你将无法再次创建自己的基地。 - -oarc-no-shared-avail=当前没有可用的共享基地。 -oarc-join-check-again=再次检查 -oarc-shared-spawn-disabled=本模式中禁用了共享生成。 - -oarc-buddy-spawn=和好友一起出生 -oarc-buddy-spawn-info=好友系统需要两个玩家同时在这个菜单中,你们将彼此并肩生成,各自拥有自己的资源。 - -oarc-max-players-shared-spawn=如果你创建自己的出生基地,则最多可以允许多达 __1__ 名其他在线玩家加入。 -oarc-spawn-dist-notes=近距离生成距离地图中心 __1__-__2__ 块。\n远距离生成距离地图中心 __3__-__4__ 块。\n单人出生有危险!要开荒到达其他玩家的地方,请做好准备。 - -oarc-player-is-joining-main-force=__ 1__正在加入主要团队! -oarc-player-is-joining-near=__1__ 从远处加入了游戏! -oarc-player-is-joining-far=___1__ 从很远的地方加入游戏! - -oarc-please-wait=请稍候,正在生成您的出生点! - -oarc-looking-for-buddy=__1__正在寻找队友。 - -oarc-avail-bases-join=可加入的基地: -oarc-spawn-spots-remaining=__1__(还剩 __2__ 个名额) -oarc-cancel-return-to-previous=取消(返回到之前的选项) -oarc-player-requesting-join-you=__1__ 请求加入你的基地! -oarc-waiting-for-spawn-owner=等待基地拥有者的回应... - -oarc-you-will-spawn-once-host=一旦对方大佬选择是,你便会加入... -oarc-player-cancel-join-request=__1__ 取消了他加入你基地的请求。 - -oarc-spawn-ctrl=生成控制 -oarc-spawn-controls=生成控制选项: -oarc-spawn-allow-joiners=允许其他人加入你的基地。 - -oarc-set-respawn-loc=设置新的重生点(有冷却时间) -oarc-set-respawn-loc-cooldown=重生点冷却剩余时间:__1__ -oarc-set-respawn-note=这将把你的重生点设置为你当前的位置。 - -oarc-select-player-join-queue=从申请队列中选择一个玩家: - -oarc-accept=欣然接受 -oarc-reject=残忍拒绝 - -oarc-no-player-join-reqs=目前没有玩家请求加入你的基地。 - -oarc-start-shared-base=新玩家现在可以加入 __1__的基地! -oarc-stop-shared-base=新玩家不能再加入 __1__ 的基地! - -oarc-spawn-point-updated=重生点已更新! -oarc-selected-player-not-wait=选中的玩家不再等待加入。 -oarc-reject-joiner=您已拒绝 __1__ 加入您的基地的请求。 -oarc-your-request-rejected=您的加入请求被拒绝了。 - -oarc-player-joining-base=__1__ 正在加入 __2__ 的基地! - -oarc-player-left-while-joining=__1__ 退出了游戏,真是无情。 - -oarc-buddy-spawn-options=好友生成选项 -oarc-buddy-spawn-instructions=要使用此功能,请确保您和您的好友同时处于此菜单中。只需其中一人发送请求即可。从列表中选择你的好友(如果看不到好友的名字,请刷新),然后选择您的出生选项。单击请求按钮发送请求。然后,另一个伙伴可以接受(或拒绝)该请求。这将使你们俩都能在彼此旁边生成,每人都有自己的出生点。一旦伙伴接受了生成请求,便不可更改! - -oarc-buddy-select-info=首先,从待选列表中选择一个好友。然后选择生成选项并发送您的请求: - -oarc-buddy-refresh=刷新好友列表 -oarc-create-buddy-team=创建你自己的好友团队 (好友和你共享研究) - -oarc-buddy-spawn-near=请求和好友出生 (近) -oarc-buddy-spawn-far=请求和好友出生 (远) - -oarc-invalid-buddy=你尚未选择有效的好友!请再试一遍。 -oarc-buddy-not-avail=已选择的好友不再可用!请再试一遍。 - -oarc-waiting-for-buddy=正在等待好友响应... -oarc-wait-buddy-select-yes=一旦您的好友选择是,您将会出生… - -oarc-buddy-cancel-request=__1__ 取消了其好友请求! - -oarc-buddy-requesting-from-you=__1__ 正在向你请求好友生成! - -oarc-buddy-txt-main-team=默认团队 -oarc-buddy-txt-new-teams=独立团队 -oarc-buddy-txt-buddy-team=好友团队 -oarc-buddy-txt-moat=被护城河环绕 -oarc-buddy-txt-near=靠近地图中心! -oarc-buddy-txt-far=远离地图中心! -oarc-buddy-txt-would-like=希望加入 -oarc-buddy-txt-next-to-you=在您旁边 - -oarc-buddy-declined=__1__ 拒绝了你的好友请求! - -oarc-spawn-wait=请稍候! -oarc-wait-text=您的出生点正在创建中。\n您将在 __1__ 秒内被传送到那里!\n请稍候… diff --git a/scenarios/OARC/control.lua b/scenarios/OARC/control.lua new file mode 100644 index 0000000..07b33ae --- /dev/null +++ b/scenarios/OARC/control.lua @@ -0,0 +1,46 @@ +-- To edit this scenario, you must make a copy of it and place it in your own scenarios folder first! +-- Do not edit the scenario provided by the mod install directly! + +-- I provide this empty scenario to avoid the freeplay scenario extra baggage. +-- You can use the freeplay scenario too just fine if you want. +-- The main benefit of the scenario is that it lets you modify the any of the config during on_init of the mod. + +-- This is where you can modify what resources spawn, how much, where, etc. +-- Once you have a config you like, it's a good idea to save it for later use so you don't lose it if you update the +-- scenario. I will try to avoid making breaking changes to this, but no guarantees. + +-- To see what settings are available, look at the config_mod.lua file in the mod folder. + +-- Check if the OARC mod is loaded. Other than that, it's an empty scenario! +script.on_init(function(event) + if not game.active_mods["oarc-mod"] then + error("OARC mod not found! This scenario is intended to be run with the OARC mod!") + end +end) + + +local oarc_scenario_interface = +{ + get_scenario_settings = function() + + ---@type OarcConfig + local modified_settings = remote.call("oarc_mod", "get_mod_settings") + + -- Overwrite whatever settings you want here: + -- If you provide an invalid value for a mod setting, it will error and not load the scenario. + ---------------------------------------------------------------------------------------------------------------- + modified_settings.server_info.welcome_msg_title = "THIS IS A TEMPLATE SCENARIO" + modified_settings.server_info.welcome_msg = "This is a template scenario. You can modify the settings in the control.lua file. If you are seeing this message, you did not modify the scenario correctly." + + modified_settings.spawn_general.shape = "circle" + + -- Some examples of overriding surface config (which is not accessible from the mod settings!) + modified_settings.surfaces_config["nauvis"].starting_items.player_start_items = { + ["coal"] = 1, -- You're on the naughty list! + } + ---------------------------------------------------------------------------------------------------------------- + return modified_settings + end +} + +remote.add_interface("oarc_scenario", oarc_scenario_interface) \ No newline at end of file diff --git a/description.json b/scenarios/OARC/description.json similarity index 100% rename from description.json rename to scenarios/OARC/description.json diff --git a/image.png b/scenarios/OARC/image.png similarity index 100% rename from image.png rename to scenarios/OARC/image.png diff --git a/scenarios/OARC/locale/en/locale-scenario.cfg b/scenarios/OARC/locale/en/locale-scenario.cfg new file mode 100644 index 0000000..e170617 --- /dev/null +++ b/scenarios/OARC/locale/en/locale-scenario.cfg @@ -0,0 +1,3 @@ +scenario-name=OARC-TEMPLATE + +description=This is an EMPTY TEMPLATE scenario! It is provided as an example of how to override the default freeplay scenario if you want to. Additionally, experienced server hosts can configure all settings from a file instead of through the mod settings. Please read the control.lua file inside the scenario for notes. [color=red]To edit this scenario, you must make a copy of it and place it in your own scenarios folder first. Do not edit this scenario directly.[/color] diff --git a/settings.lua b/settings.lua new file mode 100644 index 0000000..7bcd7ff --- /dev/null +++ b/settings.lua @@ -0,0 +1,403 @@ +data:extend({ + { + type = "bool-setting", + name = "oarc-mod-default-allow-spawning-on-other-surfaces", + setting_type = "startup", + default_value = false, + order = "a1" + }, + { + type = "string-setting", + name = "oarc-mod-main-force-name", + setting_type = "startup", + default_value = "Main Force", + order = "a2" + }, + { + type = "int-setting", + name = "oarc-mod-linked-chest-size", + setting_type = "startup", + default_value = 100, + minimum_value = 1, + maximum_value = 1000, + order = "a3" + }, + + { + type = "string-setting", + name = "oarc-mod-welcome-msg-title", + setting_type = "runtime-global", + default_value = "Insert Server Title Here!", + order = "a1" + }, + { + type = "string-setting", + name = "oarc-mod-welcome-msg", + setting_type = "runtime-global", + default_value = "Insert Server Welcome Message Here!", + order = "a2" + }, + { + type = "string-setting", + name = "oarc-mod-discord-invite", + setting_type = "runtime-global", + default_value = "Insert Discord Invite Here!", + order = "a4" + }, + + + { + type = "bool-setting", + name = "oarc-mod-enable-main-team", + setting_type = "runtime-global", + default_value = true, + order = "b1" + }, + { + type = "bool-setting", + name = "oarc-mod-enable-separate-teams", + setting_type = "runtime-global", + default_value = true, + order = "b2" + }, + + { + type = "bool-setting", + name = "oarc-mod-allow-moats-around-spawns", + setting_type = "runtime-global", + default_value = true, + order = "b4" + }, + { + type = "bool-setting", + name = "oarc-mod-enable-moat-bridging", + setting_type = "runtime-global", + default_value = false, + order = "b5" + }, + + { + type = "int-setting", + name = "oarc-mod-minimum-distance-to-existing-chunks", + setting_type = "runtime-global", + default_value = 10, + minimum_value = 5, + maximum_value = 25, + order = "c1" + }, + { + type = "int-setting", + name = "oarc-mod-near-spawn-distance", + setting_type = "runtime-global", + default_value = 100, + minimum_value = 50, + maximum_value = 250, + order = "c2" + }, + { + type = "int-setting", + name = "oarc-mod-far-spawn-distance", + setting_type = "runtime-global", + default_value = 500, + minimum_value = 250, + maximum_value = 5000, + order = "c3" + }, + + { + type = "bool-setting", + name = "oarc-mod-enable-buddy-spawn", + setting_type = "runtime-global", + default_value = true, + order = "d1" + }, + { + type = "bool-setting", + name = "oarc-mod-enable-offline-protection", + setting_type = "runtime-global", + default_value = true, + order = "d2" + }, + { + type = "bool-setting", + name = "oarc-mod-enable-shared-team-vision", + setting_type = "runtime-global", + default_value = true, + order = "d3" + }, + { + type = "bool-setting", + name = "oarc-mod-enable-shared-team-chat", + setting_type = "runtime-global", + default_value = true, + order = "d4" + }, + { + type = "bool-setting", + name = "oarc-mod-enable-shared-spawns", + setting_type = "runtime-global", + default_value = true, + order = "d5" + }, + { + type = "int-setting", + name = "oarc-mod-number-of-players-per-shared-spawn", + setting_type = "runtime-global", + default_value = 3, + minimum_value = 2, + maximum_value = 10, + order = "d6" + }, + { + type = "bool-setting", + name = "oarc-mod-enable-friendly-fire", + setting_type = "runtime-global", + default_value = false, + order = "d7" + }, + + { + type = "string-setting", + name = "oarc-mod-default-surface", + setting_type = "runtime-global", + default_value = "nauvis", + order = "e2" + }, + { + type = "bool-setting", + name = "oarc-mod-enable-secondary-spawns", + setting_type = "runtime-global", + default_value = false, + order = "e3" + }, + + { + type = "bool-setting", + name = "oarc-mod-scale-resources-around-spawns", + setting_type = "runtime-global", + default_value = true, + order = "f1" + }, + { + type = "bool-setting", + name = "oarc-mod-modified-enemy-spawning", + setting_type = "runtime-global", + default_value = true, + order = "f2" + }, + { + type = "double-setting", + name = "oarc-mod-modified-enemy-easy-evo", + setting_type = "runtime-global", + default_value = 0, + minimum_value = 0, + maximum_value = 1, + order = "f21" + }, + { + type = "double-setting", + name = "oarc-mod-modified-enemy-medium-evo", + setting_type = "runtime-global", + default_value = 0.3, + minimum_value = 0, + maximum_value = 1, + order = "f22" + }, + + { + type = "int-setting", + name = "oarc-mod-minimum-online-time", + setting_type = "runtime-global", + default_value = 15, + minimum_value = 0, + maximum_value = 60, + order = "f3" + }, + { + type = "int-setting", + name = "oarc-mod-respawn-cooldown-min", + setting_type = "runtime-global", + default_value = 5, + minimum_value = 0, + maximum_value = 60, + order = "f4" + }, + + { + type = "bool-setting", + name = "oarc-mod-enable-shared-power", + setting_type = "runtime-global", + default_value = false, + order = "f10" + }, + { + type = "bool-setting", + name = "oarc-mod-enable-shared-chest", + setting_type = "runtime-global", + default_value = false, + order = "f11" + }, + + + { + type = "bool-setting", + name = "oarc-mod-enable-regrowth", + setting_type = "runtime-global", + default_value = false, + order = "g1" + }, + { + type = "bool-setting", + name = "oarc-mod-enable-world-eater", + setting_type = "runtime-global", + default_value = false, + order = "g2" + }, + { + type = "bool-setting", + name = "oarc-mod-enable-abandoned-base-cleanup", + setting_type = "runtime-global", + default_value = true, + order = "g3" + }, + { + type = "int-setting", + name = "oarc-mod-regrowth-cleanup-interval-min", + setting_type = "runtime-global", + default_value = 60, + minimum_value = 15, + maximum_value = 180, + order = "g4" + }, + + { + type = "int-setting", + name = "oarc-mod-spawn-general-radius-tiles", + setting_type = "runtime-global", + default_value = 64, + minimum_value = 32, + maximum_value = 320, + order = "h1" + }, + { + type = "int-setting", + name = "oarc-mod-spawn-general-moat-width-tiles", + setting_type = "runtime-global", + default_value = 8, + minimum_value = 1, + maximum_value = 32, + order = "h2" + }, + { + type = "int-setting", + name = "oarc-mod-spawn-general-tree-width-tiles", + setting_type = "runtime-global", + default_value = 5, + minimum_value = 1, + maximum_value = 32, + order = "h3" + }, + { + type = "string-setting", + name = "oarc-mod-spawn-general-enable-resources-circle-shape", + setting_type = "runtime-global", + default_value = "circle", + allowed_values = {"circle", "square"}, + order = "h4" + }, + { + type = "bool-setting", + name = "oarc-mod-spawn-general-enable-force-grass", + setting_type = "runtime-global", + default_value = false, + order = "h5" + }, { + type = "string-setting", + name = "oarc-mod-spawn-general-shape", + setting_type = "runtime-global", + default_value = "circle", + allowed_values = {"circle", "octagon", "square"}, + order = "h6" + }, + + + { + type = "bool-setting", + name = "oarc-mod-resource-placement-enabled", + setting_type = "runtime-global", + default_value = true, + order = "i1" + }, + { + type = "int-setting", + name = "oarc-mod-resource-placement-distance-to-edge", + setting_type = "runtime-global", + default_value = 20, + minimum_value = 0, + maximum_value = 96, + order = "i2" + }, + { + type = "double-setting", + name = "oarc-mod-resource-placement-angle-offset", + setting_type = "runtime-global", + default_value = 2.32, + minimum_value = 0, + maximum_value = 6.28, + order = "i3" + }, + { + type = "double-setting", + name = "oarc-mod-resource-placement-angle-final", + setting_type = "runtime-global", + default_value = 4.46, + minimum_value = 0, + maximum_value = 6.28, + order = "i4" + }, + { + type = "int-setting", + name = "oarc-mod-resource-placement-vertical-offset", + setting_type = "runtime-global", + default_value = 20, + minimum_value = 0, + maximum_value = 96, + order = "i5" + }, + { + type = "int-setting", + name = "oarc-mod-resource-placement-horizontal-offset", + setting_type = "runtime-global", + default_value = 20, + minimum_value = 0, + maximum_value = 96, + order = "i6" + }, + { + type = "int-setting", + name = "oarc-mod-resource-placement-linear-spacing", + setting_type = "runtime-global", + default_value = 6, + minimum_value = 0, + maximum_value = 32, + order = "i7" + }, + { + type = "double-setting", + name = "oarc-mod-resource-placement-size-multiplier", + setting_type = "runtime-global", + default_value = 1.0, + minimum_value = 0, + maximum_value = 10, + order = "i8" + }, + { + type = "double-setting", + name = "oarc-mod-resource-placement-amount-multiplier", + setting_type = "runtime-global", + default_value = 1.0, + minimum_value = 0, + maximum_value = 10, + order = "i9" + }, +}) \ No newline at end of file diff --git a/thumbnail.png b/thumbnail.png new file mode 100644 index 0000000..b13ff6c Binary files /dev/null and b/thumbnail.png differ