├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature-change-request.md │ └── new-feature-request.md └── workflows │ └── ci.yaml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── Audio ├── alert-enemy-presence-high-zapsplat-trimmed-science_fiction_alarm_fast_high_pitched_warning_tone_emergency_003_60104.wav ├── alert-enemy-presence-low-zapsplat-modified_multimedia_game_tone_short_bright_futuristic_beep_action_tone_002_59161.wav ├── alert-structure-damaged-zapsplat-modified-emergency_alarm_003.wav ├── car-horn-zapsplat_transport_car_horn_single_beep_external_toyota_corolla_002_18246.wav ├── inventory-edge-zapsplat_vehicles_car_roof_light_switch_click_002_80933.wav ├── inventory-move-zapsplat_vehicles_bicycle_gears_lever_click_change_006_79726.wav ├── inventory-move.ogg ├── inventory-wrap-around-zapsplat_leisure_toy_plastic_wind_up_003_13198.wav ├── player-aim-locked-zapsplat_multimedia_game_beep_high_pitched_generic_002_25862.wav ├── player-bump-alert-zapsplat-trimmed_multimedia_game_sound_synth_digital_tone_beep_001_38533.wav ├── player-bump-slide-zapsplat_foley_footstep_boot_kick_gravel_stones_out_002.wav ├── player-bump-stuck-alert-zapsplat_multimedia_game_sound_synth_digital_tone_beep_005_38537.wav ├── player-bump-trip-zapsplat-trimmed_industrial_tool_pick_axe_single_hit_strike_wood_tree_trunk_001_103466.wav ├── player-crafting-zapsplat-modified_industrial_mechanical_wind_up_manual_001_86125.wav ├── player-damaged-character-zapsplat-modified_multimedia_beep_harsh_synth_single_high_pitched_87498.wav ├── player-damaged-shield-zapsplat_multimedia_game_sound_sci_fi_futuristic_beep_action_tone_001_64989.wav ├── player-mine-zapsplat_industrial_axe_or_similar_tool_hit_gravel_ground_001_103131.wav ├── player-mine_02.ogg ├── player-teleported-zapsplat_science_fiction_computer_alarm_single_medium_ring_beep_fast_004_84296.wav ├── player-turned-1face_dir.ogg ├── player-walk-zapsplat-little_robot_sound_factory_fantasy_Footstep_Dirt_001.wav ├── scanner-pulse-zapsplat_science_fiction_computer_alarm_single_medium_ring_beep_fast_001_84293.wav ├── tank-horn-zapsplat-Blastwave_FX_FireTruckHornHonk_SFXB.458.wav ├── train-alert-high-zapsplat-trimmed_science_fiction_alarm_warning_buzz_harsh_large_reverb_60111.wav ├── train-alert-low-zapsplat_multimedia_beep_digital_high_tech_electronic_001_87483.wav ├── train-clack-zapsplat-cut-transport_steam_train_arrive_at_station_with_tannoy_announcement.wav ├── train-honk-long-pixabay-modified-diesel-horn-02-98042.wav ├── train-honk-long-pixabay-modified-lower-diesel-horn-02-98042.wav └── train-honk-short-2x-GotLag.ogg ├── CHANGES.md ├── Graphics └── invisible.png ├── LICENSE ├── README.md ├── changelog.txt ├── config_changes ├── AB_initial.ini ├── AC_comprehensive.ini ├── AD_additionals.ini ├── AE_ghost_building_0_8_1.ini └── AF_0_13_1_G_key.ini ├── control.lua ├── data-updates.lua ├── data.lua ├── devdocs └── scanner.md ├── helper-scripts └── tutorial-to-md.py ├── info.json ├── locale └── en │ ├── descriptions.cfg │ ├── fa-tutorial.cfg │ ├── factorio-access.cfg │ └── launcher.cfg ├── math-helpers.lua ├── proposals └── trainstrings.md ├── scenarios ├── fa-demo-map-0-compass-valley-blank │ ├── blueprint.zip │ ├── control.lua │ ├── description.json │ ├── freeplay.lua │ ├── info.json │ └── locale │ │ └── en │ │ └── compass_valley.cfg ├── fa-demo-map-1-early-systems-version-2 │ ├── blueprint.zip │ ├── description.json │ ├── info.json │ └── locale │ │ └── en │ │ └── locale.cfg ├── fa-demo-map-2 │ ├── blueprint.zip │ ├── description.json │ ├── info.json │ └── locale │ │ └── en │ │ └── locale.cfg ├── fa-demo-map-3-more-systems │ ├── blueprint.zip │ ├── description.json │ ├── info.json │ └── locale │ │ └── en │ │ └── locale.cfg └── fa-sandbox-world-1 │ ├── blueprint.zip │ ├── control.lua │ ├── description.json │ ├── freeplay.lua │ ├── info.json │ ├── locale │ └── en │ │ └── freeplay.cfg │ └── script.dat ├── scripts ├── blueprints.lua ├── building-tools.lua ├── building-vehicle-sectors.lua ├── circuit-networks.lua ├── combat.lua ├── consts.lua ├── crafting.lua ├── data-to-runtime-map.lua ├── descriptors.lua ├── driving.lua ├── ds │ ├── circular-options-list.lua │ ├── clusterer.lua │ ├── deque.lua │ ├── sparse-bitset.lua │ └── tile-clusterer.lua ├── electrical.lua ├── equipment.lua ├── fa-info.lua ├── fa-settings-menus.lua ├── fa-utils.lua ├── field-ref.lua ├── functools.lua ├── global-manager.lua ├── graphics.lua ├── kruise-kontrol-wrapper.lua ├── localising.lua ├── memosort.lua ├── menu-search.lua ├── methods.lua ├── mining-tools.lua ├── mouse.lua ├── quickbar.lua ├── rail-builder.lua ├── rails.lua ├── rulers.lua ├── scanner │ ├── backends │ │ ├── resource-patches.lua │ │ ├── simple.lua │ │ ├── single-entity.lua │ │ ├── trees.lua │ │ └── water.lua │ ├── entrypoint.lua │ ├── scanner-consts.lua │ └── surface-scanner.lua ├── spidertron.lua ├── table-helpers.lua ├── teleport.lua ├── train-stops.lua ├── trains.lua ├── transport-belts.lua ├── travel-tools.lua ├── tutorial-system.lua ├── type-decls.lua ├── ui │ └── low-level │ │ └── multistate-switch.lua ├── uid.lua ├── warnings.lua ├── work-queue.lua ├── worker-robots.lua └── zoom.lua ├── serpent.lua ├── settings-updates.lua ├── stylua.toml └── thumbnail.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | * eol=lf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## **1. Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | ## **2. Steps to Reproduce** 14 | 1. first action 15 | 2. next action 16 | 17 | ## **3. Expected correct behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | ## **4. Mod version** 21 | Example: 0.11.2 22 | 23 | ## **5. Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-change-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature change request 3 | about: Suggest a change for a feature this project. 4 | title: '' 5 | labels: enhance existing feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## **1. Describe the existing behavior and what problems it does or might create** 11 | A clear and concise description. 12 | 13 | ## **2. Describe the change you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | ## **3. Describe alternatives you've considered, if any, perhaps including previous discussions** 17 | A clear and concise description. 18 | 19 | ## **4. Additional context** 20 | Add any other context here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: new feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## **1. Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | ## **2. Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | ## **3. Describe alternatives you've considered, if any, perhaps including previous discussions** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | ## **4. Additional context** 20 | Add any other context here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: {} 3 | pull_request: {} 4 | 5 | jobs: 6 | check-format: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | - name: 'stylua --check' 12 | shell: bash 13 | run: | 14 | cargo install --version 0.20.0 stylua --features lua52 15 | stylua --check . -v 16 | 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.code-workspace 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "justarandomgeek.factoriomod-debug", 7 | "sumneko.lua", 8 | "JohnnyMorganz.stylua", 9 | ], 10 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 11 | "unwantedRecommendations": [] 12 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "factoriomod", 9 | "request": "launch", 10 | "name": "Factorio Mod Debug", 11 | "modsPath": "${fileWorkspaceFolder}\\..\\", 12 | "factorioArgs": [ 13 | "--load-game", 14 | "test.zip" 15 | ] 16 | }, 17 | { 18 | "type": "factoriomod", 19 | "request": "launch", 20 | "name": "Factorio Mod Debug (Settings & Data)", 21 | "hookSettings": true, 22 | "hookData": true 23 | }, 24 | { 25 | "type": "factoriomod", 26 | "request": "launch", 27 | "name": "Factorio Mod Debug (Profile)", 28 | "hookMode": "profile" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": true, 3 | "editor.tabSize": 3, 4 | "Lua.diagnostics.globals": [ 5 | "players", 6 | "printout", 7 | "get_selected_ent", 8 | "direction_lookup", 9 | "cursor_highlight", 10 | "sync_build_cursor_graphics", 11 | "get_direction_of_that_from_this", 12 | "clear_obstacles_in_rectangle", 13 | "ENT_TYPES_YOU_CAN_BUILD_OVER" 14 | ], 15 | "Lua.diagnostics.disable": [ 16 | "lowercase-global", 17 | "deprecated", 18 | "need-check-nil" 19 | ], 20 | "Lua.workspace.library": [ 21 | "c:\\Program Files\\Factorio\\data" 22 | ], 23 | "Lua.workspace.checkThirdParty": "ApplyInMemory", 24 | "Lua.runtime.version": "Lua 5.2", 25 | "[lua]": { 26 | "editor.defaultFormatter": "JohnnyMorganz.stylua" 27 | } 28 | } -------------------------------------------------------------------------------- /Audio/alert-enemy-presence-high-zapsplat-trimmed-science_fiction_alarm_fast_high_pitched_warning_tone_emergency_003_60104.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/alert-enemy-presence-high-zapsplat-trimmed-science_fiction_alarm_fast_high_pitched_warning_tone_emergency_003_60104.wav -------------------------------------------------------------------------------- /Audio/alert-enemy-presence-low-zapsplat-modified_multimedia_game_tone_short_bright_futuristic_beep_action_tone_002_59161.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/alert-enemy-presence-low-zapsplat-modified_multimedia_game_tone_short_bright_futuristic_beep_action_tone_002_59161.wav -------------------------------------------------------------------------------- /Audio/alert-structure-damaged-zapsplat-modified-emergency_alarm_003.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/alert-structure-damaged-zapsplat-modified-emergency_alarm_003.wav -------------------------------------------------------------------------------- /Audio/car-horn-zapsplat_transport_car_horn_single_beep_external_toyota_corolla_002_18246.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/car-horn-zapsplat_transport_car_horn_single_beep_external_toyota_corolla_002_18246.wav -------------------------------------------------------------------------------- /Audio/inventory-edge-zapsplat_vehicles_car_roof_light_switch_click_002_80933.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/inventory-edge-zapsplat_vehicles_car_roof_light_switch_click_002_80933.wav -------------------------------------------------------------------------------- /Audio/inventory-move-zapsplat_vehicles_bicycle_gears_lever_click_change_006_79726.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/inventory-move-zapsplat_vehicles_bicycle_gears_lever_click_change_006_79726.wav -------------------------------------------------------------------------------- /Audio/inventory-move.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/inventory-move.ogg -------------------------------------------------------------------------------- /Audio/inventory-wrap-around-zapsplat_leisure_toy_plastic_wind_up_003_13198.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/inventory-wrap-around-zapsplat_leisure_toy_plastic_wind_up_003_13198.wav -------------------------------------------------------------------------------- /Audio/player-aim-locked-zapsplat_multimedia_game_beep_high_pitched_generic_002_25862.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/player-aim-locked-zapsplat_multimedia_game_beep_high_pitched_generic_002_25862.wav -------------------------------------------------------------------------------- /Audio/player-bump-alert-zapsplat-trimmed_multimedia_game_sound_synth_digital_tone_beep_001_38533.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/player-bump-alert-zapsplat-trimmed_multimedia_game_sound_synth_digital_tone_beep_001_38533.wav -------------------------------------------------------------------------------- /Audio/player-bump-slide-zapsplat_foley_footstep_boot_kick_gravel_stones_out_002.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/player-bump-slide-zapsplat_foley_footstep_boot_kick_gravel_stones_out_002.wav -------------------------------------------------------------------------------- /Audio/player-bump-stuck-alert-zapsplat_multimedia_game_sound_synth_digital_tone_beep_005_38537.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/player-bump-stuck-alert-zapsplat_multimedia_game_sound_synth_digital_tone_beep_005_38537.wav -------------------------------------------------------------------------------- /Audio/player-bump-trip-zapsplat-trimmed_industrial_tool_pick_axe_single_hit_strike_wood_tree_trunk_001_103466.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/player-bump-trip-zapsplat-trimmed_industrial_tool_pick_axe_single_hit_strike_wood_tree_trunk_001_103466.wav -------------------------------------------------------------------------------- /Audio/player-crafting-zapsplat-modified_industrial_mechanical_wind_up_manual_001_86125.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/player-crafting-zapsplat-modified_industrial_mechanical_wind_up_manual_001_86125.wav -------------------------------------------------------------------------------- /Audio/player-damaged-character-zapsplat-modified_multimedia_beep_harsh_synth_single_high_pitched_87498.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/player-damaged-character-zapsplat-modified_multimedia_beep_harsh_synth_single_high_pitched_87498.wav -------------------------------------------------------------------------------- /Audio/player-damaged-shield-zapsplat_multimedia_game_sound_sci_fi_futuristic_beep_action_tone_001_64989.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/player-damaged-shield-zapsplat_multimedia_game_sound_sci_fi_futuristic_beep_action_tone_001_64989.wav -------------------------------------------------------------------------------- /Audio/player-mine-zapsplat_industrial_axe_or_similar_tool_hit_gravel_ground_001_103131.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/player-mine-zapsplat_industrial_axe_or_similar_tool_hit_gravel_ground_001_103131.wav -------------------------------------------------------------------------------- /Audio/player-mine_02.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/player-mine_02.ogg -------------------------------------------------------------------------------- /Audio/player-teleported-zapsplat_science_fiction_computer_alarm_single_medium_ring_beep_fast_004_84296.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/player-teleported-zapsplat_science_fiction_computer_alarm_single_medium_ring_beep_fast_004_84296.wav -------------------------------------------------------------------------------- /Audio/player-turned-1face_dir.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/player-turned-1face_dir.ogg -------------------------------------------------------------------------------- /Audio/player-walk-zapsplat-little_robot_sound_factory_fantasy_Footstep_Dirt_001.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/player-walk-zapsplat-little_robot_sound_factory_fantasy_Footstep_Dirt_001.wav -------------------------------------------------------------------------------- /Audio/scanner-pulse-zapsplat_science_fiction_computer_alarm_single_medium_ring_beep_fast_001_84293.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/scanner-pulse-zapsplat_science_fiction_computer_alarm_single_medium_ring_beep_fast_001_84293.wav -------------------------------------------------------------------------------- /Audio/tank-horn-zapsplat-Blastwave_FX_FireTruckHornHonk_SFXB.458.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/tank-horn-zapsplat-Blastwave_FX_FireTruckHornHonk_SFXB.458.wav -------------------------------------------------------------------------------- /Audio/train-alert-high-zapsplat-trimmed_science_fiction_alarm_warning_buzz_harsh_large_reverb_60111.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/train-alert-high-zapsplat-trimmed_science_fiction_alarm_warning_buzz_harsh_large_reverb_60111.wav -------------------------------------------------------------------------------- /Audio/train-alert-low-zapsplat_multimedia_beep_digital_high_tech_electronic_001_87483.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/train-alert-low-zapsplat_multimedia_beep_digital_high_tech_electronic_001_87483.wav -------------------------------------------------------------------------------- /Audio/train-clack-zapsplat-cut-transport_steam_train_arrive_at_station_with_tannoy_announcement.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/train-clack-zapsplat-cut-transport_steam_train_arrive_at_station_with_tannoy_announcement.wav -------------------------------------------------------------------------------- /Audio/train-honk-long-pixabay-modified-diesel-horn-02-98042.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/train-honk-long-pixabay-modified-diesel-horn-02-98042.wav -------------------------------------------------------------------------------- /Audio/train-honk-long-pixabay-modified-lower-diesel-horn-02-98042.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/train-honk-long-pixabay-modified-lower-diesel-horn-02-98042.wav -------------------------------------------------------------------------------- /Audio/train-honk-short-2x-GotLag.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Audio/train-honk-short-2x-GotLag.ogg -------------------------------------------------------------------------------- /Graphics/invisible.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/Graphics/invisible.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Factorio Access Mod Project 2 | MIT License 3 | 4 | Copyright (c) 2024 Crimso 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------------------------- 2 | Version: 0.15.3 3 | Date: 2024.11.23 4 | 5 | Summary: 6 | - This is a quick patch update of scanner and locale features while work continues on base game 2.0 compatibility. 7 | 8 | --------------------------------------------------------------------------------------------------- 9 | Version: 0.15.2 10 | Date: 2024.10.26 11 | 12 | Summary: 13 | - This quick update improves the grouping of the new scanner and fixes some bugs in the scanner and the launcher. 14 | 15 | --------------------------------------------------------------------------------------------------- 16 | Version: 0.15.1 17 | Date: 2024.10.19 18 | 19 | Summary: 20 | - This quick follow-up update fixes a launcher bug and re-adds the scanner tool feature of 21 | grouping chests by their contents. 22 | 23 | --------------------------------------------------------------------------------------------------- 24 | Version: 0.15.0 25 | Date: 2024.10.19 26 | 27 | Summary: 28 | - The main highlight of this update is a full rewrite of the software of the scanner tool. 29 | It now incrementally scans in the background, which makes scan refreshes significantly faster, 30 | especially on larger maps. It also has smarter ways of grouping objects. 31 | 32 | - IMPORTANT: If you upgrade into an existing save, the new scanner tool will need to set up 33 | for the first time. This will take around 1 minute before it starts returning results. 34 | This does not happen on newly created maps or on maps where the setup has already been done before. 35 | However, this might happen again after future updates that signficantly modify the new scanner tool. 36 | 37 | --------------------------------------------------------------------------------------------------- 38 | Version: 0.14.1 39 | Date: 2024.09.07 40 | 41 | Summary: 42 | - This small update adds, re-adds, or improves minor features, such as player and vehicle 43 | health and armor info, infinity pipe filter setting, basic game console access, 44 | and mining drill drop chute position info. 45 | 46 | --------------------------------------------------------------------------------------------------- 47 | Version: 0.14.0 48 | Date: 2024.09.02 49 | 50 | Summary: 51 | - This update brings extensive improvements to combat and blueprint books, with many 52 | smaller additions and changes regarding other areas such as vehicles, rails, and menu search. 53 | Some keybinds have changed regarding checking character health and vehicle features. 54 | 55 | --------------------------------------------------------------------------------------------------- 56 | Version: 0.13.1 57 | Date: 2024.08.13 58 | 59 | Summary: 60 | - This is a quick update to fix a bug where pressing the research queue keys in other contexts 61 | would incorrectly report info about the research queue. 62 | 63 | --------------------------------------------------------------------------------------------------- 64 | Version: 0.13.0 65 | Date: 2024.08.11 66 | 67 | Summary: 68 | - This update introduces a prototype for audio rulers and greatly improves Kruise Kontrol 69 | functionality thanks to our transition to Kruise Kontrol Remote, a fork created by our 70 | developer @ahicks92. It also remaps some keys and improves nudging. There are other small 71 | changes such as better ghost mining and better entity selection and some tutorial corrections 72 | by @danielw97. 73 | 74 | - Note that Factorio Access is from now optionally dependent on Kruise Kontrol Remote while it 75 | is no longer compatible with other versions of Kruise Kontrol, which players need to disable 76 | or delete. Otherwise Factorio Access will not run. This only applies if you are upgrading. 77 | New installs don't need to worry about it. 78 | 79 | --------------------------------------------------------------------------------------------------- 80 | Version: 0.12.3 81 | Date: 2024.07.04 82 | 83 | Summary: 84 | - This is another quick patch update that fixes a crash introduced in 0.12.1 and some older bugs. 85 | 86 | --------------------------------------------------------------------------------------------------- 87 | Version: 0.12.2 88 | Date: 2024.07.02 89 | 90 | Summary: 91 | - This is a quick patch update that fixes an old menu switching bug that affected 92 | both menu search, and the recently added inventory slot filter setting. 93 | 94 | --------------------------------------------------------------------------------------------------- 95 | Version: 0.12.1 96 | Date: 2024.07.02 97 | 98 | Summary: 99 | - This intermediate update adds support setting item filters for player and vehicle cargo 100 | inventory slots. It also allows automating rocket silo launches and tunes the rail crossing alert. 101 | Finally, it fixes a launcher bug that makes the game freeze after configuring it successfully. 102 | 103 | --------------------------------------------------------------------------------------------------- 104 | Version: 0.12.0 105 | Date: 2024.06.28 106 | 107 | Summary: 108 | - After a detailed write-up by @ahicks and several community discussions, this update covers 109 | many topics. Most significantly, it adds support for the copy paste tool. Among other things, 110 | it enhances cursor skipping and improves building functions and various info tools. The update 111 | also fixes several crashes and bugs. 112 | 113 | --------------------------------------------------------------------------------------------------- 114 | Version: 0.11.2 115 | Date: 2024.05.17 116 | 117 | Summary: 118 | - This minor update brings several small new features, tweaks, and bugfixes across the board 119 | based on recent feedback, with special thanks this week to @ahicks. Improvements include 120 | better info and controls regarding personal logistics, rail signals, the scanner list, 121 | and the Kruise Kontrol feature. 122 | 123 | --------------------------------------------------------------------------------------------------- 124 | Version: 0.11.1 125 | Date: 2024.05.13 126 | 127 | Summary: 128 | - This update features the second phase of the refactoring of the codebase into modules, 129 | making it easier to follow and maintain. It is also accompanied by some small additions, 130 | changes, and bugfixes. Improved systems include the launcher, Remote View, Kruise Kontrol, 131 | fast travel, and the rail builder. While a fair amount of testing has been done, new bugs may 132 | still emerge due to the refactor. 133 | 134 | --------------------------------------------------------------------------------------------------- 135 | Version: 0.11.0 136 | Date: 2024.04.27 137 | 138 | Summary: 139 | - This update is mostly about changes under the hood while the mod codebase is being refactored 140 | so that the code is easier to follow and maintain, for contributors both new and old. 141 | The update also includes some bug fixes and small improvements, but the refactor has caused 142 | new bugs. Extensive testing is needed to find and fix the new bugs. Therefore this release is 143 | labeled as "experimental" and is recommended only to players who are interested in bug hunting. 144 | 145 | --------------------------------------------------------------------------------------------------- 146 | Version: 0.10.1 147 | Date: 2024.04.13 148 | 149 | Summary: 150 | - This update comes after some restructuring of the mod repository and joining the official 151 | Factorio Mod Portal. Note that releases still need to be installed from the GitHub page 152 | because of the launcher and config changes required to run the mod properly. 153 | The update itself includes tweaks, additions, and bugfixes across the board thanks to 154 | community feedback. Notably, several blueprint bugs have been fixed and Remote View has been added. 155 | 156 | --------------------------------------------------------------------------------------------------- 157 | Version: 0.10.0 158 | Date: 2024.04.06 159 | 160 | Summary: 161 | - This update features a full rewrite of the mod tutorial to include new chapters and details. 162 | Please note that the new tutorial may need more tweaking despite being reviewed, so feel free 163 | to get in touch about issues or suggestions. The update also has some launcher improvements 164 | such as mod management, as well as small additions and changes to improve the early game, 165 | and some changes thanks to community feedback. 166 | 167 | - Note: The full changelog can be found in "CHANGES.md". This covers older updates and also the 168 | sections other than the summaries. 169 | 170 | -------------------------------------------------------------------------------- /config_changes/AB_initial.ini: -------------------------------------------------------------------------------- 1 | ; Semicolons (;) at the beginning of the line indicate a comment 2 | ; However comment lines are used to document the changes. 3 | ; The comment line(s) that immediately proceed a setting are the justification 4 | ; this justification is displayed to the user verbatim if approving changes interactivly 5 | 6 | ;changes must be in the correct section to get applied correctly. 7 | ;empty sections can be left out 8 | 9 | [other] 10 | ; example: this comment is just a comment since there's a line break before the justification 11 | 12 | ;The GUI check update interupts launch flow. 13 | ;Updating will soon be available from launcher anyway. 14 | check-updates=false 15 | 16 | ;example: both of the above justification lines will be displayed in interactive mode. 17 | 18 | [controls] 19 | 20 | ;This is an additional keybind for a mouse action. 21 | mine-alternative=X 22 | 23 | ;This is an additional keybind for a mouse action. 24 | copy-entity-settings-alternative=SHIFT + RIGHTBRACKET 25 | 26 | ;This is an additional keybind for a mouse action. 27 | paste-entity-settings-alternative=SHIFT + LEFTBRACKET 28 | 29 | ; We remove this keybind to make our mod features use it. 30 | ; F1 is used by the mod for manual saving. 31 | controller-gui-logistics-tab= 32 | 33 | ;This is an additional keybind for a mouse action. 34 | fast-entity-transfer-alternative=CONTROL + LEFTBRACKET 35 | 36 | ;This is an additional keybind for a mouse action. 37 | fast-entity-split-alternative=CONTROL + RIGHTBRACKET 38 | 39 | ; We remove this keybind to make our mod features use it. 40 | ; The mod uses X for mining. 41 | rotate-active-quick-bars= 42 | 43 | ; We remove this keybind to make our mod features use it. 44 | ; The mod uses T to give time and technology progress info. 45 | ; In vanilla mode it works as it usually does. 46 | open-technology-gui= 47 | 48 | ; We remove this keybind to make our mod features use it. 49 | ; P is used for the warning menu. 50 | ; You can use top-right screen graphical buttons to open this graphical menu. 51 | production-statistics= 52 | 53 | ; We remove this keybind to make our mod features use it. 54 | ; You can use top-right screen graphical buttons to open this graphical menu. 55 | logistic-networks= 56 | 57 | ; We remove this keybind to make our mod features use it. 58 | ; V is used for the fast travel menu. You can instead disconnect a vehicle by selecting it with the cursor and pressing SHIFT + G. 59 | disconnect-train= 60 | 61 | [sound] 62 | 63 | ; We turned down the music volume. It is nice but a bit hard to hear over. 64 | ; You can turn it off completely by setting this value to 0. 65 | music-volume=0.100000 66 | 67 | [graphics] 68 | 69 | ;High graphics settings can be demanding, but if you're streaming it will look nicer. 70 | ;Options: high, normal 71 | graphics-quality=normal 72 | 73 | ;These simulations make noise and can be highly distracting. 74 | ;They would start playing before launching multiplayer or contiuing your previous session. 75 | show-game-simulations-in-background=false 76 | -------------------------------------------------------------------------------- /config_changes/AC_comprehensive.ini: -------------------------------------------------------------------------------- 1 | ; Semicolons (;) at the beginning of the line indicate a comment 2 | ; However comment lines are used to document the changes. 3 | ; The comment line(s) that immediately proceed a setting are the justification 4 | ; this justification is displayed to the user verbatim if approving changes interactivly 5 | 6 | ;changes must be in the correct section to get applied correctly. 7 | ;empty sections can be left out 8 | 9 | [other] 10 | 11 | ; This setting normally makes newly installed mods enable themselves, including Factorio Access and compatible mods. 12 | ; Due to how Factorio Access is organized, the setting always enables all installed mods. 13 | ; If you have incompatible mods installed, whether they are activated or not, you should set this to false. 14 | enable-new-mods=true 15 | 16 | [interface] 17 | 18 | ; This is a graphical setting only. Only one quickbar can be used at a time in the mod, so displaying only one at a time would make good parity. 19 | active-quick-bars=1 20 | 21 | ; This is a graphical setting only. It resizes the shortcut bar to match the suggested quickbar size. 22 | shortcut-bar-rows=1 23 | 24 | ; Set this to true if you use the smart pippette tool and you prefer to get a ghost version of an entity in hand when you have zero units of it in your inventory. 25 | pick-ghost-cursor=false 26 | 27 | ; This is set to false because this particular system is graphical and not accessible and it leads to visual clutter. 28 | show-tips-and-tricks-notifications=false 29 | 30 | ; This is set to false because this particular system is graphical and not accessible and it leads to visual clutter. 31 | enable-recipe-notifications=false 32 | 33 | [controls] 34 | 35 | ;This control has been restored to the default after earlier changes. 36 | clear-cursor=Q 37 | 38 | ;This control has been restored to the default after earlier changes. 39 | smart-pipette=Q 40 | 41 | ; We remove this keybind to make our mod features use it. 42 | ; The graphical map can still be opened by clicking on the graphical minimap. 43 | toggle-map= 44 | 45 | ; We remove this keybind to make our mod features use it. 46 | ; You can use top-right screen graphical buttons to open this graphical menu. 47 | toggle-blueprint-library= 48 | 49 | ; We remove this keybind to make our mod features use it. 50 | ; You can use top-right screen graphical buttons to open this graphical menu. 51 | open-trains-gui= 52 | 53 | ; We remove this keybind to make our mod features use it. 54 | ; The ESCAPE key can still be used to pause the game. 55 | pause-game= 56 | 57 | [sound] 58 | 59 | ; This setting make mod sounds work independent of the zoom level. 60 | zoom-volume-coefficient=0.0000 61 | 62 | [graphics] 63 | 64 | ; If you set this to true, the game will boot much faster in general but it will take 2-5 GB of extra hard disk space. 65 | cache-sprite-atlas=false 66 | 67 | ; This is set to false to improve performance. 68 | show-smoke=false 69 | 70 | ; This is set to false to improve performance. 71 | show-decoratives=false 72 | 73 | ; This is set to false to improve performance. 74 | show-particles=false 75 | 76 | ; Options: true, false (minimal effect on performance) 77 | ; show-item-shadows=true 78 | 79 | ; This is set to false to improve performance. 80 | show-inserter-shadows=false 81 | 82 | ; This is set to false to improve performance. 83 | full-color-depth=false 84 | -------------------------------------------------------------------------------- /config_changes/AD_additionals.ini: -------------------------------------------------------------------------------- 1 | ; Read previous files to review the formating used here. 2 | 3 | [controls] 4 | 5 | ; This control adds an additional way to use this input 6 | order-to-follow-alternative=CONTROL + LEFTBRACKET 7 | 8 | [sound] 9 | 10 | ; We make walking sounds louder because they need to stand out. 11 | walking-sound-volume=1.000000 12 | 13 | -------------------------------------------------------------------------------- /config_changes/AE_ghost_building_0_8_1.ini: -------------------------------------------------------------------------------- 1 | ; Read previous files to review the formating used here. 2 | 3 | [controls] 4 | 5 | ;This is an additional keybind for a mouse action. 6 | build-ghost-alternative=SHIFT + LEFTBRACKET 7 | 8 | 9 | -------------------------------------------------------------------------------- /config_changes/AF_0_13_1_G_key.ini: -------------------------------------------------------------------------------- 1 | ; Read previous files to review the formating used here. 2 | 3 | [controls] 4 | 5 | ; We use a mod feature for this via a different keybind. 6 | connect-train= 7 | 8 | ; We use a mod feature for this via a different keybind. 9 | disconnect-train= 10 | 11 | 12 | -------------------------------------------------------------------------------- /data-updates.lua: -------------------------------------------------------------------------------- 1 | local Consts = require("scripts.consts") 2 | local DataToRuntimeMap = require("scripts.data-to-runtime-map") 3 | 4 | for name, proto in pairs(data.raw.container) do 5 | proto.open_sound = proto.open_sound or { filename = "__base__/sound/metallic-chest-open.ogg", volume = 0.43 } 6 | proto.close_sound = proto.close_sound or { filename = "__base__/sound/metallic-chest-close.ogg", volume = 0.43 } 7 | end 8 | 9 | ---Apply universal belt immunity 10 | data.raw.character.character.has_belt_immunity = true 11 | 12 | ---Make the character unlikely to be selected by the mouse pointer when overlapping with entities 13 | data.raw.character.character.selection_priority = 2 14 | 15 | for _, item in pairs(vanilla_tip_and_tricks_item_table) do 16 | remove_tip_and_tricks_item(item) 17 | end 18 | 19 | -- Modifications to Kruise Kontrol inputs (no longer needed) 20 | -- We will handle Kruise Kontrol driving through the remote API. It binds 21 | -- everything to the mouse, which we don't use. The exception is enter, which 22 | -- cancels. We also cancel on enter, but double-cancel doesn't do anything. 23 | -- This file used to modify those inputs, but we don't need to since things 24 | -- already work. If we do need to revisit that, note that we will need to move 25 | -- KK inputs to a dummy key, or alternatively try setting [alt]_key_sequence to 26 | -- the empty string. Other solutions (e.g. removal, setting them to disabled) 27 | -- break KK because Factorio will not let KK register events. 28 | 29 | --Modifications to Pavement Driving Assist Continued inputs 30 | data:extend({ 31 | { 32 | type = "custom-input", 33 | name = "toggle_drive_assistant", 34 | key_sequence = "L", 35 | consuming = "game-only", 36 | }, 37 | { 38 | type = "custom-input", 39 | name = "toggle_cruise_control", 40 | key_sequence = "O", 41 | consuming = "game-only", 42 | }, 43 | { 44 | type = "custom-input", 45 | name = "set_cruise_control_limit", 46 | key_sequence = "CONTROL + O", 47 | consuming = "game-only", 48 | }, 49 | { 50 | type = "custom-input", 51 | name = "confirm_set_cruise_control_limit", 52 | key_sequence = "", 53 | linked_game_control = "confirm-gui", 54 | }, 55 | }) 56 | 57 | --Modify base prototypes to remove their default descriptions 58 | for name, pack in pairs(data.raw.tool) do 59 | if pack.localised_description and pack.localised_description[1] == "item-description.science-pack" then 60 | pack.localised_description = nil 61 | end 62 | end 63 | 64 | for name, mod in pairs(data.raw.module) do 65 | if 66 | mod.localised_description and mod.localised_description[1] == "item-description.effectivity-module" 67 | or mod.localised_description and mod.localised_description[1] == "item-description.productivity-module" 68 | or mod.localised_description and mod.localised_description[1] == "item-description.speed-module" 69 | then 70 | mod.localised_description = nil 71 | end 72 | end 73 | 74 | ---Make selected vanilla objects not collide with players 75 | local function remove_player_collision(ent_p) 76 | local new_mask = {} 77 | for _, layer in pairs(ent_p.collision_mask or { "object-layer", "floor-layer", "water-tile" }) do 78 | if layer ~= "player-layer" then table.insert(new_mask, layer) end 79 | end 80 | ent_p.collision_mask = new_mask 81 | end 82 | for _, ent_type in pairs({ "pipe", "pipe-to-ground", "constant-combinator", "inserter" }) do 83 | for _, ent_p in pairs(data.raw[ent_type]) do 84 | remove_player_collision(ent_p) 85 | end 86 | end 87 | --TODO:should probably just filter electric poles by their collision_box size... 88 | remove_player_collision(data.raw["electric-pole"]["small-electric-pole"]) 89 | remove_player_collision(data.raw["electric-pole"]["medium-electric-pole"]) 90 | 91 | --[[ 92 | We will now inject a trigger on entity creation, which will send control.lua an 93 | event on the creation of any map-placed entity. This is slow, and it should 94 | also be possible to tone it back in future if that ever becomes problematic. The 95 | purpose is being able to scan efficiently, rather than trying to scan surfaces 96 | every time we get a request. See scripts.scanner.entrypoint. 97 | 98 | A trigger is either a single trigger or an array of triggers. To be compatible 99 | with other mods, we convert these to arrays, then tack ours on at the end. 100 | ]] 101 | 102 | local function augment_with_trigger(proto) 103 | -- our trigger. 104 | ---@type data.Trigger 105 | local nt = { 106 | 107 | type = "direct", 108 | action_delivery = { 109 | type = "instant", 110 | source_effects = { 111 | type = "script", 112 | effect_id = Consts.NEW_ENTITY_SUBSCRIBER_TRIGGER_ID, 113 | }, 114 | }, 115 | } 116 | 117 | if not proto.created_effect then 118 | proto.created_effect = {} 119 | elseif not proto.created_effect[1] then 120 | -- This is how we ask lua if something is an array. 121 | proto.created_effect = { proto.created_effect } 122 | end 123 | 124 | table.insert(proto.created_effect, nt) 125 | end 126 | 127 | for ty, children in pairs(data.raw) do 128 | if not defines.prototypes.entity[ty] then goto continue end 129 | 130 | for _name, proto in pairs(children) do 131 | augment_with_trigger(proto) 132 | end 133 | ::continue:: 134 | end 135 | 136 | --[[ 137 | See https://forums.factorio.com/viewtopic.php?f=28&t=114820 138 | 139 | We need resource_patch_search_radius to write the scanner algorithm, though 140 | hopefully in future we can just ask the engine. The problem of today is that we 141 | don't have it at runtime. We therefore make a dummy item, and smuggle it 142 | across in the localised_description. The format is: 143 | 144 | prototype-name=5 145 | other-prototype-name=10 146 | 147 | So on. Parsed back out in scripts.scanner.resource-patches.lua. 148 | 149 | If nil we just don't write anything after the =. 150 | ]] 151 | 152 | local resource_search_radiuses = {} 153 | 154 | for name, proto in pairs(data.raw["resource"]) do 155 | if proto.type == "resource" then resource_search_radiuses[name] = proto.resource_patch_search_radius or 3 end 156 | end 157 | 158 | local DataToRuntimeMap = require("scripts.data-to-runtime-map") 159 | DataToRuntimeMap.build(Consts.RESOURCE_SEARCH_RADIUSES_MAP_NAME, resource_search_radiuses) 160 | -------------------------------------------------------------------------------- /helper-scripts/tutorial-to-md.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A tool for creating a markdown document from the built-in factorio access tutorial. Can be used to easily create a wiki page from the tutorial. 3 | Reads the tutorial steps from the english factorio access tutorial file fa-tutorial.cfg. Gets the default english controls from the mod's data.lua file and inserts them to the tutorial steps. 4 | To use just run from the repository root and the resulting tutorial.md is created there. 5 | ''' 6 | 7 | import configparser 8 | import re 9 | from pathlib import Path 10 | 11 | # where to find the required mod files 12 | mod_path = Path(__file__).parent.parent 13 | data_lua_path = mod_path / 'data.lua' 14 | tutorial_path = mod_path / 'locale' / 'en' / 'fa-tutorial.cfg' 15 | missing_control_count = 0 16 | controls_section_found = False 17 | 18 | # controls that could not be extracted from data.lua 19 | # new ones can be inserted if needed. The tool prints names of controls it could not find. 20 | extra_controls = { 21 | 'clear-cursor': 'Q' 22 | } 23 | 24 | 25 | def extract_controls_from_lua(file_path): 26 | ''' 27 | Gets the controls from data.lua and stores them in two dictionaries. 28 | First has the control name as key and second the linked_game_control used in some tutorial steps. 29 | ''' 30 | global controls_section_found 31 | controls_dict = {} 32 | alt_controls = {} 33 | in_controls_section = False # are we in the file section that has the controls 34 | in_control = False # are we processing a control 35 | linked_control = None # linked game control name 36 | 37 | try: 38 | with open(file_path, 'r') as lua_file: 39 | for line in lua_file: 40 | # Check if we're entering the controls section 41 | if "--New custom input events--" in line: 42 | print("Entered data.lua controls section.") 43 | in_controls_section = True 44 | controls_section_found = True 45 | elif in_controls_section: 46 | # Check if the current section has ended and we are done 47 | if "})" in line: 48 | print("Finished data.lua controls section.") 49 | break 50 | # Look for lines that define control attributes 51 | # todo update to work if this does not come first before other control attributes 52 | if "type = \"custom-input\"" in line: 53 | in_control = True 54 | elif "name" in line and in_control: 55 | # Extract the control name 56 | name = line.split("\"")[1] 57 | elif "linked_game_control" in line and in_control: 58 | # Extract the linked control name 59 | linked_control = line.split("\"")[1] 60 | elif "key_sequence" in line and 'alternative_key_sequence' not in line and in_control: 61 | # Extract the key sequence 62 | key_sequence = line.split("\"")[1] 63 | elif "}" in line and in_control: 64 | # one control processed add to dicts 65 | controls_dict[name] = key_sequence 66 | if linked_control is not None: 67 | alt_controls[linked_control] = key_sequence 68 | 69 | # prepare for next possible control 70 | linked_control = None 71 | 72 | in_control = False 73 | if not controls_section_found: 74 | print("Error: Failed to locate controls section in data.lua") 75 | return controls_dict, alt_controls 76 | except FileNotFoundError: 77 | print(f"File not found: {data_lua_path}") 78 | return {} 79 | 80 | 81 | def findControl(match: re.Match): 82 | ''' 83 | Helper method for replacing control names in tutorial steps with actual key commands. 84 | Parameter is a regular expression match object used in the replace operation. 85 | ''' 86 | global missing_control_count 87 | control = match.group(1) 88 | key = controls.setdefault(control, None) 89 | if key is None: 90 | key = alt_controls.setdefault(control, None) 91 | if key is None: 92 | missing_control_count += 1 93 | print(f'Warning no key found for control "{control}" and the event name was used instead ({str(missing_control_count)})') 94 | return control 95 | 96 | return key 97 | 98 | 99 | # get mod controls from data.lua so that control names can be replaced with actual keyboard commands. 100 | controls, alt_controls = extract_controls_from_lua(data_lua_path) 101 | # add manually defined controls that could not be found from data.lua 102 | controls.update(extra_controls) 103 | 104 | # Initialize configparser for parsing the tutorial file 105 | config = configparser.ConfigParser() 106 | 107 | # Load the tutorial cfg file 108 | config.read(tutorial_path) 109 | 110 | # Open the markdown file to write the output 111 | with open(Path(__file__).parent / 'tutorial.md', 'w') as md_file: 112 | # Check if 'tutorial' section exists 113 | if 'tutorial' in config: 114 | # Iterate through each item in the 'tutorial' section 115 | # todo now assumes that everything is in order, could be updated to work with chapter and step numbers to make sure 116 | for key, value in config['tutorial'].items(): 117 | # Split the key to get the chapter and step information 118 | split_key = key.split('-') 119 | if len(split_key) < 6: 120 | # not a tutorial text item 121 | continue 122 | 123 | chapter_num = split_key[2] 124 | step_num = split_key[4] 125 | kind = split_key[5] # Either 'header' or 'detail' 126 | if kind not in [ 'header', 'detail' ]: 127 | continue 128 | 129 | # For the first step's header, use it as the chapter title 130 | if step_num == '1' and kind == 'header': 131 | md_file.write(f"## {value}\n\n") 132 | elif kind == 'header': 133 | md_file.write("### " + step_num + f": {value}\n\n") 134 | elif kind == 'detail' and step_num != '1': 135 | # detail of first step is same as chapter header so do not repeat that 136 | # Replace the control names with corresponding keyboard commands 137 | formatted_value = re.sub(r"__CONTROL__(.+?)__", findControl, value) 138 | md_file.write(f"{formatted_value}\n\n") 139 | 140 | print("Conversion to markdown completed.") 141 | -------------------------------------------------------------------------------- /info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FactorioAccess", 3 | "version": "0.15.3", 4 | "title": "Factorio Access Mod", 5 | "author": "Crimso, SirFendi, Gweneph, Austin Hicks", 6 | "homepage": "https://github.com/Factorio-Access/FactorioAccess", 7 | "description": "This mod makes the game accessible to the blind and visually impaired, by converting visual features into audio features. Note that running this mod fully requires some config changes, as well as the mod's own launcher from the GitHub releases.", 8 | "factorio_version": "1.1", 9 | "dependencies": [ 10 | "? stop-on-red", 11 | "? VehicleSnap", 12 | "? PavementDriveAssistContinued", 13 | "? Kruise_Kontrol_Remote", 14 | "! Kruise_Kontrol_Updated", 15 | "! Kruise_Kontrol" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /locale/en/factorio-access.cfg: -------------------------------------------------------------------------------- 1 | #all keys should be part of category to not pollute the main namespace 2 | [fa] 3 | #direction 4 | #eg North 5 | direction=__plural_for_parameter_1_{0=North|1=NorthEast|2=East|3=SouthEast|4=South|5=SouthWest|6=West|7=NorthWest|rest=}__ 6 | 7 | #entity direction 8 | #eg facing North 9 | facing-direction=facing __plural_for_parameter_1_{0=North|1=NorthEast|2=East|3=SouthEast|4=South|5=SouthWest|6=West|7=NorthWest|rest=}__ 10 | 11 | #whats in your hand 12 | #eg fast inserter facing south 5 in hand 55 total 13 | #eg pipe 10 in hand 10 total 14 | cursor-description=__1____plural_for_parameter_2_{0=|1= __3__}__ __4__ in hand__plural_for_parameter_5_{0=|rest= __5__ total}__ 15 | cursor-description-verbose=The item type in your hand is __1__ __plural_for_parameter_2_{0=which is either not a building or doesn't support rotation|1=which if you were to build it, then it would be __3__}__. There are __4__ of them in your hand and __plural_for_parameter_5_{0=no more in your inventory|rest=you have __5__ total}__.}__ 16 | 17 | #whats in your hand 18 | empty_cursor=Emptied hand 19 | empty_cursor-verbose=You currently don't have anything in your hand. 20 | 21 | #inventory transfer to character 22 | grabbed-stuff=Grabbed __1__ 23 | 24 | #eg Iron Plate x 3456 25 | item-quantity=__1__ x __2__ 26 | 27 | #inventory failed transfer to character 28 | grabbed-nothing=Grabbed nothing 29 | 30 | #inventory transfer from charcter 31 | placed-stuff=Placed __1__ 32 | 33 | #failed transfer from character 34 | placed-nothing=Placed nothing 35 | 36 | #cordinates for parameter 1 37 | teleported-cursor-to=Teleported the cursor to __1__ 38 | 39 | #printed from scanner 40 | #eg assembly machine 1 producing iron gear wheels 5 of 17 41 | thing-producing-listpos-dirdist=__1__ __2__ __4__ __3__ 42 | 43 | #relative map possition 44 | #eg 35 northwest 45 | dir-dist=__2__ __1__ 46 | 47 | #scanner psuedo entity 48 | forest=forest 49 | 50 | #printed from scanner 51 | #eg assembly machine, example 35 northwest 52 | item_and_quantity-example-at-dirdist=__1__, example __2__ 53 | 54 | failed-inventory-limit-ajust-not-containter=Not a chest. 55 | failed-inventory-limit-ajust-no-limit=This inventory does not support limiting. 56 | 57 | #changing inventory limit on chest, __1__ will be __gui.all__ and __2__ will be 1000 when max is reached 58 | #eg 1 slot unlocked. 59 | #eg All slots unlocked. 60 | inventory-limit-status=__1__ __plural_for_parameter_2_{1=slot|rest=slots}__ unlocked. 61 | 62 | #nudging 63 | nudged-one-direction=Moved building 1 __1__. 64 | failed-to-nudge=Cannot nudge this building. 65 | 66 | #entity status stuff 67 | 68 | #health 69 | full-health=, full health 70 | percent-health=, __1__ percent health 71 | kk-state=Kruise Kontrol __1__. 72 | kk-cancel=Kruise Kontrol cancelled. 73 | kk-start=Kruise Kontrol starting, __1__. 74 | kk-done=Kruise Kontrol finished. 75 | kk-not-available=Kruise Kontrol is not installed or is using a different fork without a remote interface. 76 | kk-blueprints-not-allowed=You have a blueprint or blueprint book in your hand. Empty your hand before trying to use Kruise Kontrol. 77 | kk-not-started=Kruise Kontrol cannot do anything at this position. 78 | 79 | # "(iron ore) at (directionn), (1) of (5) 80 | scanner-full-presentation=__1__, at __2__, __3__ of __4__ 81 | 82 | scanner-needs-rescan=There are no entries left. Try rescanning. 83 | scanner-nothing-in-category=No __1__ found. Try rescanning or changing the scanner category. 84 | scanner-sorted=Sorted by distance. 85 | scanner-refreshed=Refreshed scan. 86 | scanner-refreshed-directional=Refreshed __1__ scan. 87 | # categories of the scanner, must match the values in scanner-consts.lua. 88 | scanner-category-all=All 89 | scanner-category-enemies=Enemies 90 | scanner-category-ghosts=Ghosts 91 | scanner-category-logistics_and_power=Logistics and power 92 | scanner-category-military=Military 93 | scanner-category-other=Other 94 | scanner-category-players=Players 95 | scanner-category-production=Production 96 | scanner-category-remnants=Remnants 97 | scanner-category-resources=Resources 98 | scanner-category-trains=Trains 99 | scanner-category-vehicles=Vehicles 100 | scanner-category-containers=Containers 101 | scanner-category-corpses=Corpses 102 | 103 | # announce a resource patch. "[resource type], with [current amount], 104 | # [percentage] remaining" 105 | scanner-resource-patch=__1__ with __2__, __3__ percent remaining 106 | scanner-water=Water __1__ by __2__ 107 | scanner-forest=Trees x __1__ 108 | ; For announcing spawner entries, special cased to tell us how much they are polluted. 109 | scanner-spawner-polluted-none=not polluted 110 | scanner-spawner-polluted-lightly=lightly polluted 111 | scanner-spawner-polluted-heavily=heavily polluted 112 | scanner-spawner-announce=__1__ __2__ 113 | 114 | 115 | [map-gen-preset-name] 116 | faccess-compass-valley=Compass Valley 117 | faccess-peaceful=Peaceful Mode 118 | faccess-enemies-off=No Enemies 119 | 120 | [map-gen-preset-description] 121 | faccess-compass-valley=A peaceful world with relatively favorable conditions and the same geography every time. Recommended for beginners. 122 | faccess-peaceful=Default settings, except the enemies will only attack when attacked, rather than attacking when nearby or when pollution reaches their base. 123 | faccess-enemies-off=Default settings except with no enemies at all. 124 | 125 | [control-keys] 126 | left-bracket=Left Bracket 127 | ]=Right Bracket 128 | ğ=Soft G 129 | ^=Caret 130 | -------------------------------------------------------------------------------- /locale/en/launcher.cfg: -------------------------------------------------------------------------------- 1 | [fa-l] 2 | selected=[selected] 3 | 4 | map-seed-description=The seed is used to generate all randomness.\nWith identical settings, and an identical seed, you'll get the same map.\nSet to r for a random seed. 5 | 6 | current-setting=Current value:__1__ 7 | 8 | new-setting-prompt=Enter new value below or leave blank to keep current value: 9 | 10 | list-all-langs=List all languages 11 | 12 | guessed-language=It looks like your system language is __1__. Confirm to select __1__ for Factorio and the Factorio Access Launcher. 13 | -------------------------------------------------------------------------------- /math-helpers.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Additional mathematical helpers on top of Lua's built-in math library. 3 | 4 | These are pure functions: no side effects, and return the same value out for the 5 | same values in. 6 | ]] 7 | 8 | local mod = {} 9 | 10 | --[[ 11 | Computes a 1-based modulus. 12 | 13 | In most languages, with 0-based indices, a useful way to "go in circles" such 14 | that always increasing an index iterates over an array over and over, is to do 15 | `i % len(array))` which, as i increments, will repeat from 0 to len(array) over 16 | and over. In Lua, we have one-based indices. mod1 is the same operation as % 17 | in a zero-based language, but offset so that it works with lua tables. 18 | 19 | E.g. given 1, 2, 3, 4, 5, 6, and mod1(i, 3), you get 1, 2, 3, 1, 2, 3 20 | ]] 21 | function mod.mod1(index, length) 22 | return ((index - 1) % length) + 1 23 | end 24 | 25 | return mod 26 | -------------------------------------------------------------------------------- /scenarios/fa-demo-map-0-compass-valley-blank/blueprint.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/scenarios/fa-demo-map-0-compass-valley-blank/blueprint.zip -------------------------------------------------------------------------------- /scenarios/fa-demo-map-0-compass-valley-blank/control.lua: -------------------------------------------------------------------------------- 1 | local handler = require("event_handler") 2 | handler.add_lib(require("freeplay")) 3 | handler.add_lib(require("silo-script")) 4 | -------------------------------------------------------------------------------- /scenarios/fa-demo-map-0-compass-valley-blank/description.json: -------------------------------------------------------------------------------- 1 | { 2 | "order": "0", 3 | "multiplayer-compatible": true, 4 | "is-main-game": true 5 | } 6 | -------------------------------------------------------------------------------- /scenarios/fa-demo-map-0-compass-valley-blank/freeplay.lua: -------------------------------------------------------------------------------- 1 | local util = require("util") 2 | local crash_site = require("crash-site") 3 | 4 | local created_items = function() 5 | return { 6 | ["iron-plate"] = 8, 7 | ["wood"] = 1, 8 | ["pistol"] = 1, 9 | ["firearm-magazine"] = 10, 10 | ["burner-mining-drill"] = 1, 11 | ["stone-furnace"] = 1, 12 | } 13 | end 14 | 15 | local respawn_items = function() 16 | return { 17 | ["pistol"] = 1, 18 | ["firearm-magazine"] = 10, 19 | } 20 | end 21 | 22 | local ship_items = function() 23 | return { 24 | ["firearm-magazine"] = 8, 25 | } 26 | end 27 | 28 | local debris_items = function() 29 | return { 30 | ["iron-plate"] = 8, 31 | } 32 | end 33 | 34 | local ship_parts = function() 35 | return crash_site.default_ship_parts() 36 | end 37 | 38 | local chart_starting_area = function() 39 | local r = global.chart_distance or 200 40 | local force = game.forces.player 41 | local surface = game.surfaces[1] 42 | local origin = force.get_spawn_position(surface) 43 | force.chart(surface, { { origin.x - r, origin.y - r }, { origin.x + r, origin.y + r } }) 44 | end 45 | 46 | local on_player_created = function(event) 47 | local player = game.get_player(event.player_index) 48 | util.insert_safe(player, global.created_items) 49 | 50 | if not global.init_ran then 51 | --This is so that other mods and scripts have a chance to do remote calls before we do things like charting the starting area, creating the crash site, etc. 52 | global.init_ran = true 53 | 54 | chart_starting_area() 55 | 56 | if not global.disable_crashsite then 57 | local surface = player.surface 58 | surface.daytime = 0.7 59 | crash_site.create_crash_site( 60 | surface, 61 | { -5, -6 }, 62 | util.copy(global.crashed_ship_items), 63 | util.copy(global.crashed_debris_items), 64 | util.copy(global.crashed_ship_parts) 65 | ) 66 | util.remove_safe(player, global.crashed_ship_items) 67 | util.remove_safe(player, global.crashed_debris_items) 68 | player.get_main_inventory().sort_and_merge() 69 | if player.character then player.character.destructible = false end 70 | global.crash_site_cutscene_active = true 71 | crash_site.create_cutscene(player, { -5, -4 }) 72 | return 73 | end 74 | end 75 | 76 | if not global.skip_intro then 77 | if game.is_multiplayer() then 78 | player.print(global.custom_intro_message or { "msg-intro" }) 79 | else 80 | game.show_message_dialog({ text = global.custom_intro_message or { "msg-intro" } }) 81 | end 82 | end 83 | end 84 | 85 | local on_player_respawned = function(event) 86 | local player = game.get_player(event.player_index) 87 | util.insert_safe(player, global.respawn_items) 88 | end 89 | 90 | local on_cutscene_waypoint_reached = function(event) 91 | if not global.crash_site_cutscene_active then return end 92 | if not crash_site.is_crash_site_cutscene(event) then return end 93 | 94 | local player = game.get_player(event.player_index) 95 | 96 | player.exit_cutscene() 97 | 98 | if not global.skip_intro then 99 | if game.is_multiplayer() then 100 | player.print(global.custom_intro_message or { "msg-intro" }) 101 | else 102 | game.show_message_dialog({ text = global.custom_intro_message or { "msg-intro" } }) 103 | end 104 | end 105 | end 106 | 107 | local skip_crash_site_cutscene = function(event) 108 | if not global.crash_site_cutscene_active then return end 109 | if event.player_index ~= 1 then return end 110 | local player = game.get_player(event.player_index) 111 | if player.controller_type == defines.controllers.cutscene then player.exit_cutscene() end 112 | end 113 | 114 | local on_cutscene_cancelled = function(event) 115 | if not global.crash_site_cutscene_active then return end 116 | if event.player_index ~= 1 then return end 117 | global.crash_site_cutscene_active = nil 118 | local player = game.get_player(event.player_index) 119 | if player.gui.screen.skip_cutscene_label then player.gui.screen.skip_cutscene_label.destroy() end 120 | if player.character then player.character.destructible = true end 121 | player.zoom = 1.5 122 | end 123 | 124 | local on_player_display_refresh = function(event) 125 | crash_site.on_player_display_refresh(event) 126 | end 127 | 128 | local freeplay_interface = { 129 | get_created_items = function() 130 | return global.created_items 131 | end, 132 | set_created_items = function(map) 133 | global.created_items = map or error("Remote call parameter to freeplay set created items can't be nil.") 134 | end, 135 | get_respawn_items = function() 136 | return global.respawn_items 137 | end, 138 | set_respawn_items = function(map) 139 | global.respawn_items = map or error("Remote call parameter to freeplay set respawn items can't be nil.") 140 | end, 141 | set_skip_intro = function(bool) 142 | global.skip_intro = bool 143 | end, 144 | get_skip_intro = function() 145 | return global.skip_intro 146 | end, 147 | set_custom_intro_message = function(message) 148 | global.custom_intro_message = message 149 | end, 150 | get_custom_intro_message = function() 151 | return global.custom_intro_message 152 | end, 153 | set_chart_distance = function(value) 154 | global.chart_distance = tonumber(value) 155 | or error("Remote call parameter to freeplay set chart distance must be a number") 156 | end, 157 | get_disable_crashsite = function() 158 | return global.disable_crashsite 159 | end, 160 | set_disable_crashsite = function(bool) 161 | global.disable_crashsite = bool 162 | end, 163 | get_init_ran = function() 164 | return global.init_ran 165 | end, 166 | get_ship_items = function() 167 | return global.crashed_ship_items 168 | end, 169 | set_ship_items = function(map) 170 | global.crashed_ship_items = map or error("Remote call parameter to freeplay set created items can't be nil.") 171 | end, 172 | get_debris_items = function() 173 | return global.crashed_debris_items 174 | end, 175 | set_debris_items = function(map) 176 | global.crashed_debris_items = map or error("Remote call parameter to freeplay set respawn items can't be nil.") 177 | end, 178 | get_ship_parts = function() 179 | return global.crashed_ship_parts 180 | end, 181 | set_ship_parts = function(parts) 182 | global.crashed_ship_parts = parts or error("Remote call parameter to freeplay set ship parts can't be nil.") 183 | end, 184 | } 185 | 186 | if not remote.interfaces["freeplay"] then remote.add_interface("freeplay", freeplay_interface) end 187 | 188 | local is_debug = function() 189 | local surface = game.surfaces.nauvis 190 | local map_gen_settings = surface.map_gen_settings 191 | return map_gen_settings.width == 50 and map_gen_settings.height == 50 192 | end 193 | 194 | local freeplay = {} 195 | 196 | freeplay.events = { 197 | [defines.events.on_player_created] = on_player_created, 198 | [defines.events.on_player_respawned] = on_player_respawned, 199 | [defines.events.on_cutscene_waypoint_reached] = on_cutscene_waypoint_reached, 200 | ["crash-site-skip-cutscene"] = skip_crash_site_cutscene, 201 | [defines.events.on_player_display_resolution_changed] = on_player_display_refresh, 202 | [defines.events.on_player_display_scale_changed] = on_player_display_refresh, 203 | [defines.events.on_cutscene_cancelled] = on_cutscene_cancelled, 204 | } 205 | 206 | freeplay.on_configuration_changed = function() 207 | global.created_items = global.created_items or created_items() 208 | global.respawn_items = global.respawn_items or respawn_items() 209 | global.crashed_ship_items = global.crashed_ship_items or ship_items() 210 | global.crashed_debris_items = global.crashed_debris_items or debris_items() 211 | global.crashed_ship_parts = global.crashed_ship_parts or ship_parts() 212 | 213 | if not global.init_ran then 214 | -- migrating old saves. 215 | global.init_ran = #game.players > 0 216 | end 217 | end 218 | 219 | freeplay.on_init = function() 220 | global.created_items = created_items() 221 | global.respawn_items = respawn_items() 222 | global.crashed_ship_items = ship_items() 223 | global.crashed_debris_items = debris_items() 224 | global.crashed_ship_parts = ship_parts() 225 | global.skip_intro = true 226 | if is_debug() then global.disable_crashsite = true end 227 | end 228 | 229 | return freeplay 230 | -------------------------------------------------------------------------------- /scenarios/fa-demo-map-0-compass-valley-blank/info.json: -------------------------------------------------------------------------------- 1 | null 2 | -------------------------------------------------------------------------------- /scenarios/fa-demo-map-0-compass-valley-blank/locale/en/compass_valley.cfg: -------------------------------------------------------------------------------- 1 | scenario-name=Compass Valley 2 | description=A blank version of Compass Valley. -------------------------------------------------------------------------------- /scenarios/fa-demo-map-1-early-systems-version-2/blueprint.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/scenarios/fa-demo-map-1-early-systems-version-2/blueprint.zip -------------------------------------------------------------------------------- /scenarios/fa-demo-map-1-early-systems-version-2/description.json: -------------------------------------------------------------------------------- 1 | { 2 | "order": "1", 3 | "multiplayer-compatible": true, 4 | "is-main-game": true 5 | } 6 | -------------------------------------------------------------------------------- /scenarios/fa-demo-map-1-early-systems-version-2/info.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /scenarios/fa-demo-map-1-early-systems-version-2/locale/en/locale.cfg: -------------------------------------------------------------------------------- 1 | scenario-name=Demo Map 1 - Early game systems 2 | description=Examples of machines and systems you can build with automation science and logistics science. -------------------------------------------------------------------------------- /scenarios/fa-demo-map-2/blueprint.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/scenarios/fa-demo-map-2/blueprint.zip -------------------------------------------------------------------------------- /scenarios/fa-demo-map-2/description.json: -------------------------------------------------------------------------------- 1 | { 2 | "order": "2", 3 | "multiplayer-compatible": true, 4 | "is-main-game": true 5 | } 6 | -------------------------------------------------------------------------------- /scenarios/fa-demo-map-2/info.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /scenarios/fa-demo-map-2/locale/en/locale.cfg: -------------------------------------------------------------------------------- 1 | scenario-name=Demo Map 2 - Main bus factory 2 | description=Example of a main bus factory. Requires a lot of belts but offers clear organization. -------------------------------------------------------------------------------- /scenarios/fa-demo-map-3-more-systems/blueprint.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/scenarios/fa-demo-map-3-more-systems/blueprint.zip -------------------------------------------------------------------------------- /scenarios/fa-demo-map-3-more-systems/description.json: -------------------------------------------------------------------------------- 1 | { 2 | "order": "3", 3 | "multiplayer-compatible": true, 4 | "is-main-game": true 5 | } 6 | -------------------------------------------------------------------------------- /scenarios/fa-demo-map-3-more-systems/info.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /scenarios/fa-demo-map-3-more-systems/locale/en/locale.cfg: -------------------------------------------------------------------------------- 1 | scenario-name=Demo Map 3 - Advanced systems 2 | description=This is an extended version of Map 1 that includes additional systems from the later game, like nuclear power. -------------------------------------------------------------------------------- /scenarios/fa-sandbox-world-1/blueprint.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/scenarios/fa-sandbox-world-1/blueprint.zip -------------------------------------------------------------------------------- /scenarios/fa-sandbox-world-1/control.lua: -------------------------------------------------------------------------------- 1 | local handler = require("event_handler") 2 | handler.add_lib(require("freeplay")) 3 | handler.add_lib(require("silo-script")) 4 | -------------------------------------------------------------------------------- /scenarios/fa-sandbox-world-1/description.json: -------------------------------------------------------------------------------- 1 | { 2 | "order": "a", 3 | "multiplayer-compatible": true, 4 | "is-main-game": true 5 | } 6 | -------------------------------------------------------------------------------- /scenarios/fa-sandbox-world-1/freeplay.lua: -------------------------------------------------------------------------------- 1 | local util = require("util") 2 | local crash_site = require("crash-site") 3 | 4 | local created_items = function() 5 | return { 6 | ["iron-plate"] = 8, 7 | ["wood"] = 1, 8 | ["pistol"] = 1, 9 | ["firearm-magazine"] = 10, 10 | ["burner-mining-drill"] = 1, 11 | ["stone-furnace"] = 1, 12 | } 13 | end 14 | 15 | local respawn_items = function() 16 | return { 17 | ["pistol"] = 1, 18 | ["firearm-magazine"] = 10, 19 | } 20 | end 21 | 22 | local ship_items = function() 23 | return { 24 | ["firearm-magazine"] = 8, 25 | } 26 | end 27 | 28 | local debris_items = function() 29 | return { 30 | ["iron-plate"] = 8, 31 | } 32 | end 33 | 34 | local ship_parts = function() 35 | return crash_site.default_ship_parts() 36 | end 37 | 38 | local chart_starting_area = function() 39 | local r = global.chart_distance or 200 40 | local force = game.forces.player 41 | local surface = game.surfaces[1] 42 | local origin = force.get_spawn_position(surface) 43 | force.chart(surface, { { origin.x - r, origin.y - r }, { origin.x + r, origin.y + r } }) 44 | end 45 | 46 | local on_player_created = function(event) 47 | local player = game.get_player(event.player_index) 48 | util.insert_safe(player, global.created_items) 49 | 50 | if not global.init_ran then 51 | --This is so that other mods and scripts have a chance to do remote calls before we do things like charting the starting area, creating the crash site, etc. 52 | global.init_ran = true 53 | 54 | chart_starting_area() 55 | 56 | if not global.disable_crashsite then 57 | local surface = player.surface 58 | surface.daytime = 0.7 59 | crash_site.create_crash_site( 60 | surface, 61 | { -5, -6 }, 62 | util.copy(global.crashed_ship_items), 63 | util.copy(global.crashed_debris_items), 64 | util.copy(global.crashed_ship_parts) 65 | ) 66 | util.remove_safe(player, global.crashed_ship_items) 67 | util.remove_safe(player, global.crashed_debris_items) 68 | player.get_main_inventory().sort_and_merge() 69 | if player.character then player.character.destructible = false end 70 | global.crash_site_cutscene_active = true 71 | crash_site.create_cutscene(player, { -5, -4 }) 72 | return 73 | end 74 | end 75 | 76 | if not global.skip_intro then 77 | if game.is_multiplayer() then 78 | player.print(global.custom_intro_message or { "msg-intro" }) 79 | else 80 | game.show_message_dialog({ text = global.custom_intro_message or { "msg-intro" } }) 81 | end 82 | end 83 | end 84 | 85 | local on_player_respawned = function(event) 86 | local player = game.get_player(event.player_index) 87 | util.insert_safe(player, global.respawn_items) 88 | end 89 | 90 | local on_cutscene_waypoint_reached = function(event) 91 | if not global.crash_site_cutscene_active then return end 92 | if not crash_site.is_crash_site_cutscene(event) then return end 93 | 94 | local player = game.get_player(event.player_index) 95 | 96 | player.exit_cutscene() 97 | 98 | if not global.skip_intro then 99 | if game.is_multiplayer() then 100 | --player.print(global.custom_intro_message or {"msg-intro"}) 101 | else 102 | --game.show_message_dialog{text = global.custom_intro_message or {"msg-intro"}} 103 | end 104 | end 105 | end 106 | 107 | local skip_crash_site_cutscene = function(event) 108 | if not global.crash_site_cutscene_active then return end 109 | if event.player_index ~= 1 then return end 110 | local player = game.get_player(event.player_index) 111 | if player.controller_type == defines.controllers.cutscene then player.exit_cutscene() end 112 | end 113 | 114 | local on_cutscene_cancelled = function(event) 115 | if not global.crash_site_cutscene_active then return end 116 | if event.player_index ~= 1 then return end 117 | global.crash_site_cutscene_active = nil 118 | local player = game.get_player(event.player_index) 119 | if player.gui.screen.skip_cutscene_label then player.gui.screen.skip_cutscene_label.destroy() end 120 | if player.character then player.character.destructible = true end 121 | player.zoom = 1.5 122 | end 123 | 124 | local on_player_display_refresh = function(event) 125 | crash_site.on_player_display_refresh(event) 126 | end 127 | 128 | local freeplay_interface = { 129 | get_created_items = function() 130 | return global.created_items 131 | end, 132 | set_created_items = function(map) 133 | global.created_items = map or error("Remote call parameter to freeplay set created items can't be nil.") 134 | end, 135 | get_respawn_items = function() 136 | return global.respawn_items 137 | end, 138 | set_respawn_items = function(map) 139 | global.respawn_items = map or error("Remote call parameter to freeplay set respawn items can't be nil.") 140 | end, 141 | set_skip_intro = function(bool) 142 | global.skip_intro = bool 143 | end, 144 | get_skip_intro = function() 145 | return global.skip_intro 146 | end, 147 | set_custom_intro_message = function(message) 148 | global.custom_intro_message = message 149 | end, 150 | get_custom_intro_message = function() 151 | return global.custom_intro_message 152 | end, 153 | set_chart_distance = function(value) 154 | global.chart_distance = tonumber(value) 155 | or error("Remote call parameter to freeplay set chart distance must be a number") 156 | end, 157 | get_disable_crashsite = function() 158 | return global.disable_crashsite 159 | end, 160 | set_disable_crashsite = function(bool) 161 | global.disable_crashsite = bool 162 | end, 163 | get_init_ran = function() 164 | return global.init_ran 165 | end, 166 | get_ship_items = function() 167 | return global.crashed_ship_items 168 | end, 169 | set_ship_items = function(map) 170 | global.crashed_ship_items = map or error("Remote call parameter to freeplay set created items can't be nil.") 171 | end, 172 | get_debris_items = function() 173 | return global.crashed_debris_items 174 | end, 175 | set_debris_items = function(map) 176 | global.crashed_debris_items = map or error("Remote call parameter to freeplay set respawn items can't be nil.") 177 | end, 178 | get_ship_parts = function() 179 | return global.crashed_ship_parts 180 | end, 181 | set_ship_parts = function(parts) 182 | global.crashed_ship_parts = parts or error("Remote call parameter to freeplay set ship parts can't be nil.") 183 | end, 184 | } 185 | 186 | if not remote.interfaces["freeplay"] then remote.add_interface("freeplay", freeplay_interface) end 187 | 188 | local is_debug = function() 189 | local surface = game.surfaces.nauvis 190 | local map_gen_settings = surface.map_gen_settings 191 | return map_gen_settings.width == 50 and map_gen_settings.height == 50 192 | end 193 | 194 | local freeplay = {} 195 | 196 | freeplay.events = { 197 | [defines.events.on_player_created] = on_player_created, 198 | [defines.events.on_player_respawned] = on_player_respawned, 199 | [defines.events.on_cutscene_waypoint_reached] = on_cutscene_waypoint_reached, 200 | ["crash-site-skip-cutscene"] = skip_crash_site_cutscene, 201 | [defines.events.on_player_display_resolution_changed] = on_player_display_refresh, 202 | [defines.events.on_player_display_scale_changed] = on_player_display_refresh, 203 | [defines.events.on_cutscene_cancelled] = on_cutscene_cancelled, 204 | } 205 | 206 | freeplay.on_configuration_changed = function() 207 | global.created_items = global.created_items or created_items() 208 | global.respawn_items = global.respawn_items or respawn_items() 209 | global.crashed_ship_items = global.crashed_ship_items or ship_items() 210 | global.crashed_debris_items = global.crashed_debris_items or debris_items() 211 | global.crashed_ship_parts = global.crashed_ship_parts or ship_parts() 212 | 213 | if not global.init_ran then 214 | -- migrating old saves. 215 | global.init_ran = #game.players > 0 216 | end 217 | end 218 | 219 | freeplay.on_init = function() 220 | global.created_items = created_items() 221 | global.respawn_items = respawn_items() 222 | global.crashed_ship_items = ship_items() 223 | global.crashed_debris_items = debris_items() 224 | global.crashed_ship_parts = ship_parts() 225 | 226 | if is_debug() then 227 | global.skip_intro = true 228 | global.disable_crashsite = true 229 | end 230 | end 231 | 232 | return freeplay 233 | -------------------------------------------------------------------------------- /scenarios/fa-sandbox-world-1/info.json: -------------------------------------------------------------------------------- 1 | null 2 | -------------------------------------------------------------------------------- /scenarios/fa-sandbox-world-1/locale/en/freeplay.cfg: -------------------------------------------------------------------------------- 1 | msg-intro=Welcome to Factorio Freeplay. 2 | scenario-name=Sandbox World 3 | description=Sandbox world with open building space and infinite resources. -------------------------------------------------------------------------------- /scenarios/fa-sandbox-world-1/script.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/scenarios/fa-sandbox-world-1/script.dat -------------------------------------------------------------------------------- /scripts/consts.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Constants for our mod. Must load in the data stage as well as runtime. 3 | ]] 4 | 5 | local mod = {} 6 | 7 | -- We inject a trigger into all entities which allows us to subscribe to their 8 | -- creation. This trigger is identified by id defined by us, and delivered in 9 | -- one event along with possible triggers for other mods. This isn't well 10 | -- documented, you could start at 11 | -- https://lua-api.factorio.com/latest/types/ScriptTriggerEffectItem.html#effect_id 12 | mod.NEW_ENTITY_SUBSCRIBER_TRIGGER_ID = "fa.subscribe-to-new-entities" 13 | 14 | mod.RESOURCE_SEARCH_RADIUSES_MAP_NAME = "resource-search-radiuses" 15 | 16 | return mod 17 | -------------------------------------------------------------------------------- /scripts/crafting.lua: -------------------------------------------------------------------------------- 1 | --Here: Crafting menu, crafting queue menu, and related functions 2 | 3 | local util = require("util") 4 | local fa_utils = require("scripts.fa-utils") 5 | local localising = require("scripts.localising") 6 | 7 | local mod = {} 8 | 9 | --Returns a navigable list of all unlocked recipes, for the recipe categories supported by the selected entity. Optionally can return all unlocked recipes for all categories. 10 | function mod.get_recipes(pindex, ent, load_all_categories) 11 | if not ent then return {} end 12 | local category_filters = {} 13 | --Load the supported recipe categories for this entity 14 | for category_name, _ in pairs(ent.prototype.crafting_categories) do 15 | table.insert(category_filters, { filter = "category", category = category_name }) 16 | end 17 | local all_machine_recipes = game.get_filtered_recipe_prototypes(category_filters) 18 | local unlocked_machine_recipes = {} 19 | local force_recipes = game.get_player(pindex).force.recipes 20 | 21 | --Load all crafting categories if instructed 22 | if load_all_categories == true then 23 | ---@diagnostic disable-next-line: cast-local-type 24 | all_machine_recipes = force_recipes 25 | end 26 | 27 | --Load only the unlocked recipes 28 | for recipe_name, recipe in pairs(all_machine_recipes) do 29 | if force_recipes[recipe_name] ~= nil and force_recipes[recipe_name].enabled then 30 | if unlocked_machine_recipes[recipe.group.name] == nil then unlocked_machine_recipes[recipe.group.name] = {} end 31 | table.insert(unlocked_machine_recipes[recipe.group.name], force_recipes[recipe.name]) 32 | end 33 | end 34 | local result = {} 35 | for group, recipes in pairs(unlocked_machine_recipes) do 36 | table.insert(result, recipes) 37 | end 38 | return result 39 | end 40 | 41 | --Reads out the selected slot of the player crafting queue. 42 | function mod.read_crafting_queue(pindex, start_phrase) 43 | start_phrase = start_phrase or "" 44 | if players[pindex].crafting_queue.max ~= 0 then 45 | local item = players[pindex].crafting_queue.lua_queue[players[pindex].crafting_queue.index] 46 | local recipe_name_only = item.recipe 47 | printout( 48 | start_phrase .. localising.get(game.recipe_prototypes[recipe_name_only], pindex) .. " x " .. item.count, 49 | pindex 50 | ) 51 | else 52 | printout(start_phrase .. "Blank", pindex) 53 | end 54 | end 55 | 56 | --Returns a count of how many batches of this recipe are listed in the (entire) crafting queue. 57 | function mod.count_in_crafting_queue(recipe_name, pindex) 58 | local count = 0 59 | if game.get_player(pindex).crafting_queue == nil or #game.get_player(pindex).crafting_queue == 0 then 60 | return count 61 | end 62 | for i, item in ipairs(game.get_player(pindex).crafting_queue) do 63 | if item.recipe == recipe_name then count = count + item.count end 64 | --game.print(item.recipe .. " vs " .. recipe_name) 65 | end 66 | return count 67 | end 68 | 69 | --Loads the crafting queue menu for a player. 70 | function mod.load_crafting_queue(pindex) 71 | if players[pindex].crafting_queue.lua_queue ~= nil then 72 | players[pindex].crafting_queue.lua_queue = game.get_player(pindex).crafting_queue 73 | if players[pindex].crafting_queue.lua_queue ~= nil then 74 | delta = players[pindex].crafting_queue.max - #players[pindex].crafting_queue.lua_queue 75 | players[pindex].crafting_queue.index = math.max(1, players[pindex].crafting_queue.index - delta) 76 | players[pindex].crafting_queue.max = #players[pindex].crafting_queue.lua_queue 77 | else 78 | players[pindex].crafting_queue.index = 1 79 | players[pindex].crafting_queue.max = 0 80 | end 81 | else 82 | players[pindex].crafting_queue.lua_queue = game.get_player(pindex).crafting_queue 83 | players[pindex].crafting_queue.index = 1 84 | if players[pindex].crafting_queue.lua_queue ~= nil then 85 | players[pindex].crafting_queue.max = #players[pindex].crafting_queue.lua_queue 86 | else 87 | players[pindex].crafting_queue.max = 0 88 | end 89 | end 90 | end 91 | 92 | --Returns a count of total recipe batches left in the player crafting queue. 93 | function mod.get_crafting_que_total(pindex) 94 | local p = game.get_player(pindex) 95 | local total_items = 0 96 | if p.crafting_queue == nil or p.crafting_queue == {} then return 0 end 97 | for i, q_item in ipairs(p.crafting_queue) do 98 | total_items = total_items + q_item.count 99 | end 100 | return total_items 101 | end 102 | 103 | --Reads the currently selected recipe in the player crafting menu. 104 | function mod.read_crafting_slot(pindex, start_phrase, new_category) 105 | start_phrase = start_phrase or "" 106 | local recipe = 107 | players[pindex].crafting.lua_recipes[players[pindex].crafting.category][players[pindex].crafting.index] 108 | if recipe.valid == true then 109 | if new_category == true then start_phrase = start_phrase .. localising.get_alt(recipe.group, pindex) .. ", " end 110 | printout( 111 | start_phrase 112 | .. localising.get_recipe_from_name(recipe.name, pindex) 113 | .. ", can craft " 114 | .. game.get_player(pindex).get_craftable_count(recipe.name), 115 | pindex 116 | ) 117 | else 118 | printout("Blank", pindex) 119 | end 120 | end 121 | 122 | --Returns an info string about how many units of which ingredients are missing in order to craft one batch of this recipe. 123 | function mod.recipe_missing_ingredients_info(pindex, recipe_in) 124 | local recipe = recipe_in 125 | or players[pindex].crafting.lua_recipes[players[pindex].crafting.category][players[pindex].crafting.index] 126 | local p = game.get_player(pindex) 127 | local inv = p.get_main_inventory() 128 | local result = "Missing " 129 | local missing = 0 130 | for i, ing in ipairs(recipe.ingredients) do 131 | local on_hand = inv.get_item_count(ing.name) 132 | local needed = ing.amount - on_hand 133 | if needed > 0 then 134 | missing = missing + 1 135 | if missing > 1 then result = result .. " and " end 136 | result = result .. needed .. " " .. localising.get_item_from_name(ing.name, pindex) 137 | end 138 | end 139 | if missing == 0 then result = "" end 140 | return result 141 | end 142 | 143 | --Returns info text on the raw ingredients for a recipe. 144 | function mod.recipe_raw_ingredients_info(recipe, pindex) 145 | local raw_ingredients = mod.get_raw_ingredients_table(recipe, pindex) 146 | --Merge duplicates 147 | local merged_table = {} 148 | for i, ing in ipairs(raw_ingredients) do 149 | local is_in_table = false 150 | for j, ingt in ipairs(merged_table) do 151 | if ingt.name == ing.name then 152 | is_in_table = true 153 | --Add the count to the existing table count. 154 | ingt.amount = ingt.amount + ing.amount 155 | end 156 | end 157 | if is_in_table == false then 158 | --Add a new table entry 159 | table.insert(merged_table, ing) 160 | end 161 | end 162 | 163 | --Construct result string 164 | local result = "Base ingredients: " 165 | for j, ingt in ipairs(merged_table) do 166 | local localised_name = ingt.name 167 | ---@type LuaItemPrototype | LuaFluidPrototype 168 | local ingredient_prototype = game.item_prototypes[ingt.name] 169 | 170 | if ingredient_prototype then 171 | localised_name = localising.get(ingredient_prototype, pindex) 172 | else 173 | ingredient_prototype = game.fluid_prototypes[ingt.name] 174 | if ingredient_prototype ~= nil then 175 | localised_name = localising.get(ingredient_prototype, pindex) 176 | else 177 | localised_name = ingt.name 178 | end 179 | end 180 | 181 | result = result .. localised_name .. ", " --" times " .. ingt.amount .. ", " 182 | end 183 | return result 184 | end 185 | 186 | --Explores a recipe and its sub-recipes and returns a table that contains all ingredients that do not have their own sub-recipes. 187 | --The same ingredient may appear multiple times in the table, so its entries need to be merged. 188 | --Bug: Due to ratios of ingredients to products across multiple recipes, the counts are not being calculated correctly, so they are ignored. 189 | function mod.get_raw_ingredients_table(recipe, pindex, count_in) 190 | local count = count_in or 1 191 | local raw_ingredients_table = {} 192 | for i, ing in ipairs(recipe.ingredients) do 193 | --Check if a recipe of the ingredient's name exists 194 | local sub_recipe = game.recipe_prototypes[ing.name] 195 | if sub_recipe ~= nil and sub_recipe.valid then 196 | --If the sub-recipe cannot be crafted by hand, add this ingredient to the main table 197 | if 198 | sub_recipe.category ~= "basic-crafting" 199 | and sub_recipe.category ~= "crafting" 200 | and sub_recipe.category ~= "" 201 | and sub_recipe.category ~= nil 202 | then 203 | for i = 1, count, 1 do 204 | table.insert(raw_ingredients_table, ing) 205 | end 206 | else 207 | --Check the sub-recipe recursively 208 | local sub_table = mod.get_raw_ingredients_table(sub_recipe, pindex) --, ing.amount) 209 | if sub_table ~= nil then 210 | --Copy the sub_table to the main table 211 | for j, ing2 in ipairs(sub_table) do 212 | for i = 1, count, 1 do 213 | table.insert(raw_ingredients_table, ing2) 214 | end 215 | end 216 | end 217 | end 218 | else 219 | --If a sub-recipe does not exist, add this ingredient to the main table 220 | for i = 1, count, 1 do 221 | table.insert(raw_ingredients_table, ing) 222 | end 223 | end 224 | end 225 | return raw_ingredients_table 226 | end 227 | 228 | return mod 229 | -------------------------------------------------------------------------------- /scripts/data-to-runtime-map.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | A data-to-runtime map is our name for a map of information only available in the 3 | data stage, which is able to be read in the runtime stage. This map holds 4 | string keys and string values only, and both keys and values must be under 200 5 | characters in length. 6 | 7 | These maps are defined by their name, which should be unique per mapping. The 8 | function build() should be called in the data stage. The function load should 9 | be called at runtime, but only after on_init. 10 | 11 | Each key-value pair is shoved into the localised string of a uniquely named item 12 | prototype of the form fa-data-map-mapname-i, where i is some numeric index 13 | (starting at 1, to match lua). Empty maps are represented with 14 | fa-data-map-empty. Maps which are not present at all throw. 15 | 16 | An example of why this is useful is our resource clustering algorithm, which 17 | needs data not yet exposed on LuaEntityPrototype. 18 | ]] 19 | 20 | local mod = {} 21 | 22 | ---@param name string 23 | ---@param values table Values have tostring called on them for you. 24 | function mod.build(name, values) 25 | local index = 1 26 | for k, v in pairs(values) do 27 | local v = tostring(v) 28 | 29 | data:extend({ 30 | { 31 | type = "item", 32 | name = string.format("fa-data-map-%s-%i", name, index), 33 | icon = data.raw.item.accumulator.icon, 34 | icon_size = 2, 35 | stack_size = 1, 36 | localised_description = { k, v }, 37 | }, 38 | }) 39 | 40 | index = index + 1 41 | end 42 | 43 | if index == 1 then 44 | -- We didn't add anything; instead, record that it was empty. 45 | 46 | values:extend({ 47 | { 48 | type = "item", 49 | name = string.format("fa-data-map-%s-empty", name), 50 | icon = values.raw.item.accumulator.icon, 51 | icon_size = 2, 52 | stack_size = 1, 53 | localised_description = "EMPTY_MAP", 54 | }, 55 | }) 56 | end 57 | end 58 | 59 | --@param name string 60 | ---@return table 61 | function mod.load(name) 62 | local res = {} 63 | local i = 1 64 | 65 | while true do 66 | local protoname = string.format("fa-data-map-%s-%i", name, i) 67 | local proto = game.item_prototypes[protoname] 68 | if not proto then break end 69 | local k = proto.localised_description[1] 70 | local v = proto.localised_description[2] 71 | assert(k) 72 | assert(v) 73 | res[k] = v 74 | i = i + 1 75 | end 76 | 77 | if i == 1 then 78 | -- It is empty. But let us make sure and fail loudly if it doesn't exist 79 | -- at all. 80 | assert(game.item_prototypes[string.format("fa-dta-map-%s-empty", name)], "Map " .. name .. " was empty") 81 | end 82 | 83 | return res 84 | end 85 | 86 | return mod 87 | -------------------------------------------------------------------------------- /scripts/descriptors.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This file contains "descriptors" of prototypes and entities. It may be split 3 | later, for example if we opt to start accounting for mods. Logic doesn't belong 4 | here: this should be as close to pure data as feasible. 5 | 6 | Most Factorio entities have some common behaviors. While there are exceptions 7 | (for example combinators, the Rocket silo), we can look at the API and see a few 8 | things. Such examples include whether or not something has health, whether or 9 | not something is a container (and if that container supports filtering), whether 10 | or not something connects to the logistic network, etc. In practice most of 11 | this handling may be generic, and in so doing it may extend itself inh some 12 | cases without our help (all inserters are "inserter" for example). 13 | 14 | Further down the road, the special cases may likely be handled here as well: 15 | that requires only a generic concept of actions and a way to extend the list on 16 | a per-prototype basis. For now, however, we restrict ourselves to really common 17 | things like the circuit network. 18 | 19 | Since this is in progress--in particular it's only circuit send modes at the 20 | moment--we leave documentation of the schema aside. Otherwise this comment will 21 | probably become stale very quickly. The exammples here should be reasonably 22 | self-explanatory. 23 | ]] 24 | local F = require("scripts.field-ref") 25 | 26 | local dcb = defines.control_behavior 27 | 28 | local mod = {} 29 | 30 | -- Prototypes, by type. 31 | mod.PROTOTYPES = { 32 | inserter = { 33 | circuit_network = { 34 | reading = { 35 | toggle_field = F.circuit_read_hand_contents(), 36 | mode_field = F.circuit_hand_read_mode(), 37 | disabled_label = "None", 38 | choices = { 39 | { dcb.inserter.hand_read_mode.hold, "Reading held items" }, 40 | { dcb.inserter.hand_read_mode.pulse, "pulsing held items" }, 41 | }, 42 | }, 43 | }, 44 | }, 45 | } 46 | 47 | return mod 48 | -------------------------------------------------------------------------------- /scripts/ds/circular-options-list.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | A circular, static list of options. 3 | 4 | This solves the problem where there's a property, it's got a few values, and 5 | each of those values has a label or something. The property could change at any 6 | time out from under the mod, and it needs to be tracked. 7 | 8 | Lists take the form: 9 | 10 | ``` 11 | { 12 | entries = { 13 | { key = defines.whatever, value = { label = "foo" } } 14 | ... and so on 15 | }, 16 | options = { 17 | comparer = func, -- defaults to ==, see below. 18 | }, 19 | } 20 | ``` 21 | 22 | Which looks like a dictionary because it is, but a poor man's ordered one. The 23 | functions in this file take such lists, and can answer the question what is 24 | previous/current/next, based on a fed-in value. 25 | 26 | sometimes, it is the case that one wishes to use a more complex key. For 27 | example, a tuple of items. In this case, one may override the comparison by 28 | setting options.comparer to a function taking two arguments, and returning true 29 | if and only if they are equal (and otherwise false). This module provides one 30 | such helper, `tuples`, which is useful in the case of state machines whose state 31 | is more than one variable. For an example of this, see 32 | ui/low-level/multistate-switch.lua, and also see the comments on tuples for 33 | additional information. 34 | 35 | A helper, kv_list, can be used like this: 36 | 37 | ``` 38 | kv_list{{ key1, value1 }, { key2, value2 }, ... } 39 | ``` 40 | 41 | To build these inline without dealing with the verbose syntax. To do this with 42 | a comparer: 43 | 44 | ``` 45 | kv_list({...}, comparer) 46 | ``` 47 | 48 | Note that during a single Lua call--a single event handler, in other words--the 49 | mod "owns" the state and fields will only change because the mod did something. 50 | 51 | This module does *not* handle the case of duplicate keys (so, no inventories; 52 | the user won't be able to get past the second instance) and it does *not* handle 53 | the case of missing values (it will hard crash intentionally). It does handle 54 | appending and reordering keys. The point is things like circuit networks where 55 | there are some fixed values and a property, not more complex things. By the 56 | fact that it doesn't handle missing values, it doesn't handle empty lists either 57 | (no values are found in an empty list). 58 | 59 | Operations are all O(N). 60 | ]] 61 | local math_helpers = require("math-helpers") 62 | 63 | local mod = {} 64 | 65 | mod.ANY = {} 66 | 67 | --[[ 68 | If the keys in this list need to be tuples, this comparer will allow for that by 69 | comparing field by field in an array. For example, { true, "value"}. 70 | 71 | A magical constant `ANY` exposed in this module will match any value. Consider 72 | this list of circuit network states: 73 | 74 | ``` 75 | { false, ANY } 76 | { true, holding } 77 | { true, pulsing } 78 | ``` 79 | 80 | If the object is off (the first state) then the second field doesn't matter, and 81 | we wish to stil move to the second entry. `ANY` is useful in this case. The 82 | requirement for safe usage is that any use of `ANY` is such that the other 83 | fields of the tuple uniquely identify the state. For example: 84 | 85 | ``` 86 | { 1, ANY } 87 | { 1, 2} 88 | ``` 89 | 90 | Is a list with duplicates because `{ 1, ANY }` matches `{ 1, 2 }`. 91 | ]] 92 | 93 | function mod.tuples(a, b) 94 | if #a ~= #b then return false end 95 | for i = 1, #a do 96 | if a[i] ~= b[i] and a[i] ~= mod.ANY and b[i] ~= mod.ANY then return false end 97 | end 98 | 99 | return true 100 | end 101 | 102 | function mod.kv_list(list, comparer) 103 | vals = {} 104 | for i = 1, #list do 105 | vals[i] = { key = list[i][1], value = list[i][2] } 106 | end 107 | return { 108 | values = vals, 109 | options = { comparer = comparer }, 110 | } 111 | end 112 | 113 | function find_key_index_or_die(list, key) 114 | local cmp = list.options.comparer or rawequal 115 | for i = 1, #list.values do 116 | if cmp(list.values[i].key, key) then return i end 117 | end 118 | 119 | error("Key " .. serpent.block(key) .. " not in this list" .. serpent.block(list)) 120 | end 121 | 122 | --[[ 123 | Returns the current item, as a table with fields key and value. For 124 | convenience, field wrapped is set to false (see prev/next for what that's for) 125 | 126 | This is basically lookup, but so named because the key is the current 127 | item--you're owning the place the index is stored, not this module, but it is 128 | still an index. 129 | ]] 130 | function mod.current(list, key) 131 | local i = find_key_index_or_die(list, key) 132 | return { key = list.values[i].key, value = list.values[i].value, wrapped = false } 133 | end 134 | 135 | --[[ 136 | next and prev are the movement functions, which move forward by one element or 137 | backward by one element, respectively. They return a table with fields key, 138 | value, and wrapped, where key and value match the keys and values in the list, 139 | and wrapped is true if this operation wrapped around to the other end of the 140 | list. 141 | ]] 142 | 143 | function mod.next(list, current_key) 144 | local i = find_key_index_or_die(list, current_key) 145 | local next_i = math_helpers.mod1(i + 1, #list.values) 146 | -- <= because lists of 1 item always wrap back to the same index. 147 | local wrapped = next_i <= i 148 | return { key = list.values[next_i].key, value = list.values[next_i].value, wrapped = wrapped } 149 | end 150 | 151 | function mod.prev(list, current_key) 152 | local i = find_key_index_or_die(list, current_key) 153 | local prev_i = math_helpers.mod1(i - 1, #list.values) 154 | -- >= because lists of 1 item always wrap back to the same index. 155 | local wrapped = prev_i >= i 156 | return { key = list.values[prev_i].key, value = list.values[prev_i].value, wrapped = wrapped } 157 | end 158 | 159 | return mod 160 | -------------------------------------------------------------------------------- /scripts/ds/deque.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | A double-ended queue based on: https://www.lua.org/pil/11.4.html 3 | 4 | That's a long resource. The idea is much simpler than the chapter: maintain two 5 | indices for the bottom and top, then use the fact that Lua allows "moving" the 6 | array upward forever at the cost of becoming a hashtable. 7 | 8 | This is global-safe. 9 | 10 | The one complexity is that Lua is one-based. We therefore copy the above's 11 | convension of `front = back = somenumber` being a queue with one item, not 0. 12 | ]] 13 | 14 | local mod = {} 15 | 16 | ---@class fa.ds.Deque 17 | ---@field front number 18 | ---@field back number 19 | ---@field items table 20 | local Deque = {} 21 | local deque_meta = { __index = Deque } 22 | 23 | -- We want to be able to poke at this outside Factorio for debugging. 24 | if script then script.register_metatable("fa.ds.Deque", deque_meta) end 25 | mod.Deque = Deque 26 | 27 | ---@returns fa.ds.Deque 28 | function Deque.new() 29 | local state = { 30 | front = 1, 31 | back = 0, 32 | items = {}, 33 | } 34 | 35 | setmetatable(state, deque_meta) 36 | return state 37 | end 38 | 39 | function Deque:push_front(item) 40 | -- Front is either pointing at nothing (empty queue) or at a valid item. 41 | local f = self.front - 1 42 | self.items[f] = item 43 | self.front = f 44 | end 45 | 46 | function Deque:push_back(item) 47 | -- Back is either pointing at a valid item or the queue is empty. 48 | local b = self.back + 1 49 | self.items[b] = item 50 | self.back = b 51 | end 52 | 53 | ---@returns bool 54 | function Deque:is_empty() 55 | return self.back < self.front 56 | end 57 | 58 | ---@returns any? 59 | function Deque:pop_front() 60 | local f = self.front 61 | local b = self.back 62 | if b < f then return nil end 63 | local r = self.items[f] 64 | self.items[f] = nil 65 | self.front = f + 1 66 | return r 67 | end 68 | 69 | ---@returns any? 70 | function Deque:pop_back() 71 | local f = self.front 72 | local b = self.back 73 | if b < f then return nil end 74 | local r = self.items[b] 75 | self.items[b] = nil 76 | self.back = b - 1 77 | return r 78 | end 79 | 80 | function Deque:clear() 81 | self.front = 1 82 | self.back = 0 83 | self.items = {} 84 | self.items = {} 85 | end 86 | 87 | -- Some quick self-tests. 88 | local test_d = Deque.new() 89 | assert(test_d:is_empty()) 90 | test_d:push_back(1) 91 | assert(test_d:pop_front() == 1) 92 | test_d:push_back(1) 93 | test_d:push_back(2) 94 | test_d:push_back(3) 95 | assert(test_d:pop_back() == 3) 96 | assert(test_d:pop_front() == 1) 97 | assert(not test_d:is_empty()) 98 | test_d:clear() 99 | assert(test_d:is_empty()) 100 | 101 | return mod 102 | -------------------------------------------------------------------------------- /scripts/ds/sparse-bitset.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | What the name says: a sparse bitset. 3 | 4 | Handles all values -2^53 to 2^53. Representation is a table of the bits divided 5 | out by 32 and values of 32 bits for each. 6 | 7 | Saves 8x min on memory over the approach of just using a table of bools. 8 | ]] 9 | 10 | local mod = {} 11 | 12 | local band = bit32.band 13 | local bor = bit32.bor 14 | local btest = bit32.btest 15 | local bnot = bit32.bnot 16 | local lshift = bit32.lshift 17 | local floor = math.floor 18 | 19 | ---@class fa.ds.SparseBitset 20 | ---@field bits table 21 | local SparseBitset = {} 22 | mod.SparseBitset = SparseBitset 23 | local SparseBitset_meta = { __index = SparseBitset } 24 | if script then script.register_metatable("fa.ds.SparseBitset", SparseBitset_meta) end 25 | 26 | ---@return fa.ds.SparseBitset 27 | function SparseBitset.new() 28 | return setmetatable({ bits = {} }, SparseBitset_meta) 29 | end 30 | 31 | ---@param bit number 32 | ---@return boolean 33 | function SparseBitset:test(bit) 34 | local chunk = floor(bit / 32) 35 | local data = self.bits[chunk] 36 | if not data then return false end 37 | local offset = bit - chunk * 32 38 | local mask = lshift(1, offset) 39 | return btest(mask, data) 40 | end 41 | 42 | ---@param bit number 43 | function SparseBitset:set(bit) 44 | local chunk = floor(bit / 32) 45 | local offset = bit - chunk * 32 46 | local data = self.bits[chunk] or 0 47 | self.bits[chunk] = bor(lshift(1, offset), data) 48 | end 49 | 50 | ---@param bit number 51 | ---@return boolean whether or not the bit used to be set 52 | function SparseBitset:remove(bit) 53 | local chunk = floor(bit / 32) 54 | local data = self.bits[chunk] 55 | if not data then return false end 56 | local offset = bit - chunk * 32 57 | local newdata = band(data, bnot(lshift(1, offset))) 58 | if newdata == 0 then 59 | self.bits[chunk] = nil 60 | else 61 | self.bits[chunk] = newdata 62 | end 63 | 64 | -- It's set if what we did cleared a bit. 65 | return data ~= newdata 66 | end 67 | 68 | return mod 69 | -------------------------------------------------------------------------------- /scripts/electrical.lua: -------------------------------------------------------------------------------- 1 | --Here: Electricity related functions and menus 2 | local util = require("util") 3 | local fa_utils = require("scripts.fa-utils") 4 | 5 | local mod = {} 6 | 7 | --Formats a power value in watts to summarize it as a string according to its magnitude. 8 | ---@param power float 9 | function mod.get_power_string(power) 10 | result = "" 11 | if power > 1000000000000 then 12 | power = power / 1000000000000 13 | result = result .. string.format(" %.1f Terawatts", power) 14 | elseif power > 1000000000 then 15 | power = power / 1000000000 16 | result = result .. string.format(" %.1f Gigawatts", power) 17 | elseif power > 1000000 then 18 | power = power / 1000000 19 | result = result .. string.format(" %.1f Megawatts", power) 20 | elseif power > 1000 then 21 | power = power / 1000 22 | result = result .. string.format(" %.1f Kilowatts", power) 23 | else 24 | result = result .. string.format(" %.1f Watts", power) 25 | end 26 | return result 27 | end 28 | 29 | --Spawns a lamp at the electric pole and uses its energy level to approximate the network satisfaction percentage with high accuracy 30 | function mod.get_electricity_satisfaction(electric_pole) 31 | local satisfaction = -1 32 | local test_lamp = electric_pole.surface.create_entity({ 33 | name = "small-lamp", 34 | position = electric_pole.position, 35 | raise_built = false, 36 | force = electric_pole.force, 37 | }) 38 | satisfaction = math.ceil(test_lamp.energy * 9 / 8) --Experimentally found coefficient 39 | test_lamp.destroy({}) 40 | return satisfaction 41 | end 42 | 43 | --For an electricity producer, returns an info string on the current and maximum production. 44 | ---@param ent LuaEntity 45 | function mod.get_electricity_flow_info(ent) 46 | local result = "" 47 | local power = 0 48 | local capacity = 0 49 | for i, v in pairs(ent.electric_network_statistics.output_counts) do 50 | power = power 51 | + ( 52 | ent.electric_network_statistics.get_flow_count({ 53 | name = i, 54 | input = false, 55 | precision_index = defines.flow_precision_index.five_seconds, 56 | }) 57 | ) 58 | local cap_add = 0 59 | for _, power_ent in pairs(ent.surface.find_entities_filtered({ name = i, force = ent.force })) do 60 | if power_ent.electric_network_id == ent.electric_network_id then cap_add = cap_add + 1 end 61 | end 62 | cap_add = cap_add * game.entity_prototypes[i].max_energy_production 63 | if game.entity_prototypes[i].type == "solar-panel" then 64 | cap_add = cap_add * ent.surface.solar_power_multiplier * (1 - ent.surface.darkness) 65 | end 66 | capacity = capacity + cap_add 67 | end 68 | power = power * 60 69 | capacity = capacity * 60 70 | result = result 71 | .. mod.get_power_string(power) 72 | .. " being produced out of " 73 | .. mod.get_power_string(capacity) 74 | .. " capacity, " 75 | return result 76 | end 77 | 78 | --Finds the neearest electric pole. Can be set to determine whether to check only for poles with electricity flow. Can call using only the first two parameters. 79 | function mod.find_nearest_electric_pole(ent, require_supplied, radius, alt_surface, alt_pos) 80 | ---@type LuaEntity 81 | local nearest = nil 82 | local min_dist = 99999 83 | require_supplied = require_supplied or false 84 | radius = radius or 10 85 | ---@type LuaSurface 86 | local surface = nil 87 | local pos = nil 88 | if ent ~= nil and ent.valid then 89 | surface = ent.surface 90 | pos = ent.position 91 | else 92 | surface = alt_surface 93 | pos = alt_pos 94 | end 95 | 96 | --Scan nearby for electric poles, expand radius if not successful 97 | local poles = surface.find_entities_filtered({ type = "electric-pole", position = pos, radius = radius }) 98 | if #poles == 0 then 99 | if radius < 100 then 100 | radius = 100 101 | return mod.find_nearest_electric_pole(ent, require_supplied, radius, alt_surface, alt_pos) 102 | elseif radius < 1000 then 103 | radius = 1000 104 | return mod.find_nearest_electric_pole(ent, require_supplied, radius, alt_surface, alt_pos) 105 | elseif radius < 10000 then 106 | radius = 10000 107 | return mod.find_nearest_electric_pole(ent, require_supplied, radius, alt_surface, alt_pos) 108 | else 109 | return nil, nil --Nothing within 10000 tiles! 110 | end 111 | end 112 | 113 | --Find the nearest among the poles with electric networks 114 | for i, pole in ipairs(poles) do 115 | --Check if the pole's network has power producers 116 | local has_power = mod.get_electricity_satisfaction(pole) > 0 117 | local dict = pole.electric_network_statistics.output_counts 118 | local network_producers = {} 119 | for name, count in pairs(dict) do 120 | table.insert(network_producers, { name = name, count = count }) 121 | end 122 | local network_producer_count = #network_producers --laterdo test again if this is working, it should pick up even 0.001% satisfaction... 123 | local dist = 0 124 | if has_power or network_producer_count > 0 or not require_supplied then 125 | dist = math.ceil(util.distance(pos, pole.position)) 126 | --Set as nearest if valid 127 | if dist < min_dist then 128 | min_dist = dist 129 | nearest = pole 130 | end 131 | end 132 | end 133 | --Return the nearst found, possibly nil 134 | if nearest == nil then 135 | if radius < 100 then 136 | radius = 100 137 | return mod.find_nearest_electric_pole(ent, require_supplied, radius, alt_surface, alt_pos) 138 | elseif radius < 1000 then 139 | radius = 1000 140 | return mod.find_nearest_electric_pole(ent, require_supplied, radius, alt_surface, alt_pos) 141 | elseif radius < 10000 then 142 | radius = 10000 143 | return mod.find_nearest_electric_pole(ent, require_supplied, radius, alt_surface, alt_pos) 144 | else 145 | return nil, nil --Nothing within 10000 tiles! 146 | end 147 | end 148 | --Draw a circle around the nearest electric pole 149 | rendering.draw_circle({ 150 | color = { 1, 1, 0 }, 151 | radius = 2, 152 | width = 2, 153 | target = nearest.position, 154 | surface = nearest.surface, 155 | time_to_live = 60, 156 | }) 157 | return nearest, min_dist 158 | end 159 | 160 | --Returns an info string on the nearest supplied electric pole for this entity. 161 | function mod.report_nearest_supplied_electric_pole(ent) 162 | local result = "" 163 | local pole, dist = mod.find_nearest_electric_pole(ent, true) 164 | local dir = -1 165 | if pole ~= nil then 166 | dir = fa_utils.get_direction_biased(pole.position, ent.position) 167 | result = "The nearest powered electric pole is " .. dist .. " tiles to the " .. fa_utils.direction_lookup(dir) 168 | else 169 | result = "And there are no powered electric poles within ten thousand tiles. Generators may be out of energy." 170 | end 171 | return result 172 | end 173 | 174 | return mod 175 | -------------------------------------------------------------------------------- /scripts/fa-settings-menus.lua: -------------------------------------------------------------------------------- 1 | --Here: Functions relating to mod settings menus. This module is WIP. 2 | --Does not include event handlers directly, but can have functions called by them. 3 | 4 | local mod = {} 5 | 6 | function mod.top_menu_open(pindex) 7 | --Load menu data 8 | local settings_menu = players[pindex].mod_menu 9 | if settings_menu == nil then 10 | settings_menu = { 11 | submenu = "", 12 | index = 0, 13 | } 14 | players[pindex].mod_menu = settings_menu 15 | end 16 | 17 | --Set the player menu tracker to this menu 18 | players[pindex].menu = "mod_menu" 19 | players[pindex].in_menu = true 20 | players[pindex].move_queue = {} 21 | 22 | --Reset the menu line index to 0 23 | players[pindex].mod_menu.index = 0 24 | 25 | --Play sound 26 | game.get_player(pindex).play_sound({ path = "Open-Inventory-Sound" }) 27 | 28 | --Load menu 29 | mod.run_top_menu(pindex, players[pindex].mod_menu.index, false) 30 | end 31 | 32 | --[[ 33 | Settings top menu--*** WIP 34 | 0. About this menu and instructions 35 | 1. Mod controls list (read only) [All controls are listed directly in game] 36 | 2. Mod preferences [Mod settings that affect presentation but have minimal gameplay changes, e.g. chest row length] 37 | 3. Advanced mod settings [Settings that can significantly impact gameplay] 38 | 4. Vanilla preferences [API-accessible preferences that match those found in the vanilla menus, if any ] 39 | ]] 40 | function mod.run_top_menu(pindex, menu_index, clicked) 41 | local index = menu_index 42 | 43 | if index == 0 then 44 | --About this menu and instructions 45 | printout( 46 | "Mod settings menu " 47 | .. ", Press 'W' and 'S' to navigate options, press 'LEFT BRACKET' to select an option or press 'E' to exit this menu.", 48 | pindex 49 | ) 50 | elseif index == 1 then 51 | --Mod controls list (read only) [All controls are listed directly in game] 52 | if not clicked then 53 | printout("Mod controls list (read only)", pindex) 54 | else 55 | --*** 56 | end 57 | elseif index == 2 then 58 | -- Mod preferences [Mod settings that affect presentation but have minimal gameplay changes, e.g. chest row length] 59 | if not clicked then 60 | printout("Mod preferences", pindex) 61 | else 62 | --*** 63 | end 64 | elseif index == 3 then 65 | -- Advanced mod settings [Settings that can significantly impact gameplay] 66 | if not clicked then 67 | printout("Advanced mod settings", pindex) 68 | else 69 | --*** 70 | end 71 | end 72 | end 73 | SETTINGS_TOP_MENU_LENGTH = 3 74 | 75 | function mod.controls_menu_open(pindex) 76 | --Load menu data 77 | local menu_data = players[pindex].fa_mod_controls_menu 78 | if menu_data == nil then 79 | menu_data = { 80 | index = 0, 81 | mod.load_mod_controls_list(pindex), 82 | } 83 | players[pindex].fa_mod_controls_menu = menu_data 84 | end 85 | 86 | --Set the player menu tracker to this menu 87 | players[pindex].menu = "fa_mod_controls_menu" 88 | players[pindex].in_menu = true 89 | 90 | --Reset the menu line index to 0 91 | players[pindex].fa_mod_controls_menu.index = 0 92 | 93 | --Play sound 94 | game.get_player(pindex).play_sound({ path = "Open-Inventory-Sound" }) 95 | 96 | --Load menu 97 | mod.run_controls_menu(pindex, players[pindex].fa_mod_controls_menu.index, false) 98 | end 99 | 100 | function mod.load_mod_controls_list(pindex) 101 | --*** 102 | end 103 | 104 | --[[ 105 | Mod controls menu 106 | 0. About this menu and instructions 107 | X. Controls, grouped by chapters, same concept as tutorial steps! 108 | ]] 109 | function mod.run_controls_menu(pindex, menu_index, clicked, pg_up, pg_down) 110 | local index = menu_index 111 | 112 | if index == 0 then 113 | --About this menu and instructions 114 | printout( 115 | "Mod controls menu, with a read-only list of mod controls " 116 | .. ", Press 'W' and 'S' to navigate options, press 'LEFT BRACKET' to select an option or press 'E' to exit this menu.", 117 | pindex 118 | ) 119 | else 120 | --...read the appropriate localized string 121 | end 122 | end 123 | MOD_CONTROLS_MENU_LENGTH = 2 124 | 125 | function mod.preferences_menu_open(pindex) 126 | --Load menu data 127 | local menu_data = players[pindex].fa_mod_preferences_menu 128 | if menu_data == nil then 129 | menu_data = { 130 | index = 0, 131 | } 132 | players[pindex].fa_mod_preferences_menu = menu_data 133 | end 134 | 135 | --Set the player menu tracker to this menu 136 | players[pindex].menu = "fa_mod_preferences_menu" 137 | players[pindex].in_menu = true 138 | 139 | --Reset the menu line index to 0 140 | players[pindex].fa_mod_preferences_menu.index = 0 141 | 142 | --Play sound 143 | game.get_player(pindex).play_sound({ path = "Open-Inventory-Sound" }) 144 | 145 | --Load menu 146 | mod.run_preferences_menu(pindex, players[pindex].fa_mod_preferences_menu.index, false) 147 | end 148 | 149 | --[[ 150 | Mod preferences menu 151 | 0. About this menu and instructions 152 | 1. Pref 1 153 | 2. Pref 2 154 | 3. Etc. 155 | ]] 156 | function mod.run_preferences_menu(pindex, menu_index, clicked, pg_up, pg_down) 157 | local index = menu_index 158 | 159 | if index == 0 then 160 | --About this menu and instructions 161 | printout( 162 | "Mod preferences menu, with settings that affect interface but have minimal gameplay changes " 163 | .. ", Press 'W' and 'S' to navigate options, press 'LEFT BRACKET' to select an option or press 'E' to exit this menu.", 164 | pindex 165 | ) 166 | elseif index == 1 then 167 | --... 168 | if not clicked then 169 | printout("Mute enemy proximity alerts", pindex) 170 | else 171 | --*** 172 | end 173 | elseif index == 2 then 174 | --... 175 | if not clicked then 176 | printout("Player inventory wrap around", pindex) 177 | else 178 | --*** 179 | end 180 | elseif index == 3 then 181 | --... 182 | if not clicked then 183 | printout("Building inventory wrap around", pindex) 184 | else 185 | --*** 186 | end 187 | elseif index == 4 then 188 | --... 189 | if not clicked then 190 | printout("Building row length", pindex) 191 | else 192 | --*** 193 | end 194 | end 195 | end 196 | MOD_PREFERENCES_MENU_LENGTH = 4 197 | 198 | --[[ 199 | Mod advanced settings menu 200 | 0. About this menu and instructions 201 | 1. Pref 1 202 | 2. Pref 2 203 | 3. Etc. 204 | ]] 205 | function mod.run_advanced_settings_menu(pindex, menu_index, clicked, pg_up, pg_down) 206 | local index = menu_index 207 | 208 | if index == 0 then 209 | --About this menu and instructions 210 | printout( 211 | "Mod advanced settings menu, with settings that strongly affect gameplay " 212 | .. ", Press 'W' and 'S' to navigate options, press 'LEFT BRACKET' to select an option or press 'E' to exit this menu.", 213 | pindex 214 | ) 215 | elseif index == 1 then 216 | --... 217 | if not clicked then 218 | printout("Triple player reach", pindex) 219 | else 220 | --*** 221 | end 222 | elseif index == 2 then 223 | --... 224 | if not clicked then 225 | printout("Peaceful mode", pindex) 226 | else 227 | --*** 228 | end 229 | elseif index == 3 then 230 | --... 231 | if not clicked then 232 | printout(" ", pindex) 233 | else 234 | --*** 235 | end 236 | end 237 | end 238 | MOD_ADVANCED_SETTINGS_MENU_LENGTH = 2 239 | 240 | return mod 241 | -------------------------------------------------------------------------------- /scripts/field-ref.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | "pointers" to fields in tables. 3 | 4 | Consider the circuit network. It has a number of control behaviors of the form 5 | [ off, or, some, other, options ], where one has a pair of fields--one for the 6 | off/on state and one for the other options. If we can refer to these fields in 7 | a generic way, then we can pass references to them around and just tell the code 8 | what labels to use this time. Much of Factorio is like this: some set of fields 9 | pretty similar to each other, and some labels/options that vary. 10 | 11 | This module returns a magic table. You use it like this: 12 | 13 | ``` 14 | local F = require('field-ref') 15 | local reference = F.a.b.c() -- The parens are required. 16 | -- reference may now be used to work with x.a.b.c on anything that has an a.b.c: 17 | local example ={ a = { b = { c = 5 } } } 18 | assert(reference.get(example) == 5) 19 | -- Or you can set it 20 | reference.set(10) 21 | assert(reference.get(example) == 10) 22 | ``` 23 | 24 | An error is thrown if the path encounters a nil on any of the intermediate steps 25 | to the final value but will return nil for the final value itself. 26 | 27 | Field references cannot be stored in global. 28 | 29 | (If you just want to use it, you can stop here. Devs who want to know how it 30 | works, read on). 31 | 32 | # Implementation 33 | 34 | Firstly, paths look like: 35 | 36 | ``` 37 | { "a", "field", "indexed", "with", 5 } 38 | -- is x.a.field.indexed.with[5] 39 | ``` 40 | 41 | Recall that lua makes no distinction between `a.b` and `a["b"]`--from the 42 | perspective of the C API they're the same thing. That's why this works on 43 | Factorio objects. 44 | 45 | The actual handling is a metatable trick. Firstly, metatables only let one 46 | intercept indices which are new. The way this works is that you can put a 47 | metatable on an empty table and that metatable can point at whatever else to 48 | override indexing, and the empty table can just remain empty forever. 49 | 50 | Now the problem is, since paths are tables technically the user could modify 51 | them after the fact by trying to continue it. To deal with this, we clone the 52 | path on every step instead of writing a new one. The actual indexing step is 53 | fast, and the performance hit in creation is fine because presumably no one 54 | wants to build these all the time. 55 | 56 | For sanity we then just assert that the user is not trying to "compile" an empty 57 | path. 58 | ]] 59 | 60 | --- @class FieldRef 61 | --- @field get fun(target: any): any 62 | --- @field set fun(target: any, value: any) 63 | 64 | --- @class FieldRefBuilder 65 | ---@field [string] FieldRefBuilder 66 | --@field [number] FieldRefBuilder 67 | ---@operator call: FieldRef 68 | 69 | -- By providing our own copy, this can be tested outside of Factorio at a shell. 70 | local function copy(path) 71 | local new = {} 72 | for i = 1, #path do 73 | table.insert(new, path[i]) 74 | end 75 | return new 76 | end 77 | 78 | -- Get a string representation of a path for error reporting purposes. 79 | local function stringify_path(path) 80 | local res = "" 81 | for p = 1, #path do 82 | local seg = path[p] 83 | if type(seg) == "string" or type(seg) == "number" or type(seg) == "boolean" then 84 | res = res .. "." .. tostring(seg) 85 | else 86 | res = res .. "" 87 | end 88 | end 89 | 90 | return res 91 | end 92 | 93 | -- Clone and append to a path. 94 | -- 95 | ---@nodiscard 96 | local function append_to_path(p, new_seg) 97 | local cloned = copy(p) 98 | table.insert(cloned, new_seg) 99 | return cloned 100 | end 101 | 102 | -- Make it not possible to set new indices in a table with = 103 | local function no_setting_meta() 104 | error("in-progress or compiled field references cannot have new fields set on them") 105 | end 106 | 107 | -- Follow a path down. If the second argument to this function is true, follow 108 | -- the path down to but not including the last segment, then return (result, 109 | -- last_segment) instead. 110 | local function follow_path(object, path, exclude_last) 111 | local length = #path 112 | if exclude_last then length = length - 1 end 113 | 114 | local ret = object 115 | for i = 1, length do 116 | ret = ret[path[i]] 117 | if ret == nil and i ~= #path then 118 | error("Attempt to follow a path, but found nil at step " .. i .. " path is " .. stringify_path(path)) 119 | end 120 | end 121 | 122 | if exclude_last then 123 | return ret, path[#path] 124 | else 125 | return ret 126 | end 127 | end 128 | 129 | -- "compile" a path into the final form, then return a table which can be used 130 | -- to manipulate it per the API docs at the top of this module. 131 | -- 132 | -- Expects the path to already be cloned. 133 | local function compile(path) 134 | assert( 135 | #path > 0, 136 | "Attempt to compile the empty/root path, e.g. `F()`. Paths must always have at least one field on them." 137 | ) 138 | 139 | local meta = { 140 | __newindex = no_setting_meta, 141 | } 142 | 143 | local funcs = { 144 | get = function(object) 145 | return follow_path(object, path, false) 146 | end, 147 | set = function(object, val) 148 | local ret, last = follow_path(object, path, true) 149 | ret[last] = val 150 | end, 151 | } 152 | 153 | return setmetatable(funcs, { __newindex = no_setting_meta }) 154 | end 155 | 156 | -- Capture a path, then return a special empty table with a metatable that lets 157 | -- one continue the chain, or compile. The path must have already been cloned. 158 | local function capture_path_and_build(path) 159 | local meta = { 160 | __index = function(_table, key) 161 | local cloned = append_to_path(path, key) 162 | return capture_path_and_build(cloned) 163 | end, 164 | __newindex = no_setting_meta, 165 | __call = function() 166 | -- We don't compile paths of length 0 and all paths after length 0 are 167 | -- cloned simply by being created, but an extra clone doesn't hurt on 168 | -- the slow, infrequent path. 169 | return compile(copy(path)) 170 | end, 171 | } 172 | 173 | -- The empty table has no keys and so will always call our metatable methods. 174 | return setmetatable({}, meta) 175 | end 176 | 177 | -- This is complicated, and self tests here have no dependency on Factorio. 178 | -- Let's just always run some when imported for lack of a proper unit testing 179 | -- framework. At least the mod won't load if this breaks, rather than failing 180 | -- at some arbitrary point in some arbitrary way. 181 | 182 | -- This variable is how it'd be imported by others, and returned at the end of 183 | -- this file. The root is just an empty path. 184 | --- @type FieldRefBuilder 185 | local F = capture_path_and_build({}) 186 | 187 | local test_value = { 188 | f1 = "f1", 189 | f2 = { 190 | f1 = "f2.f1", 191 | }, 192 | } 193 | 194 | -- No root path compilation. 195 | ok = pcall(function() 196 | F() 197 | end) 198 | assert(ok == false) 199 | 200 | -- 1 field deep works. 201 | local simple = F.f1() 202 | assert(simple.get(test_value) == "f1") 203 | simple.set(test_value, "new") 204 | assert(simple.get(test_value) == "new") 205 | assert(test_value.f1 == "new") 206 | 207 | -- 2 fields deep works. 208 | local deep = F.f2.f1() 209 | assert(deep.get(test_value) == "f2.f1") 210 | deep.set(test_value, "new2") 211 | assert(deep.get(test_value) == "new2") 212 | assert(test_value.f2.f1 == "new2") 213 | 214 | -- We can work over something mixed between strings and numbers. 215 | local mixed = { 216 | f = { 1, 2, 3 }, 217 | } 218 | local mixed_ref = F.f[2]() 219 | assert(mixed_ref.get(mixed) == 2) 220 | mixed_ref.set(mixed, "new") 221 | assert(mixed_ref.get(mixed) == "new") 222 | assert(mixed.f[2] == "new") 223 | 224 | return F 225 | -------------------------------------------------------------------------------- /scripts/functools.lua: -------------------------------------------------------------------------------- 1 | local mod = {} 2 | 3 | --[[ 4 | Return a function wrapping the original function. When called the first time, 5 | call the original function. Otherwise, return a cached value. 6 | 7 | This exists for a very simple reason: Factorio doesn't give us access to 8 | prototype metadata in the runtime stage until *after* control.lua. This stops 9 | us from easily making top-level consts in the normal fashion. Instead, we may: 10 | 11 | ``` 12 | local CONST = cached(computer) 13 | 14 | -- To get the value 15 | CONST() 16 | ``` 17 | 18 | This can be extended with support for arguments later should we desire to do so; 19 | that's backward compatible in Lua with functions taking ... 20 | ]] 21 | ---@param func function(): any 22 | ---@returns function(): any 23 | function mod.cached(func) 24 | local cache = nil 25 | -- Could actually be nil, so use a flag. 26 | local did_cache = false 27 | 28 | return function() 29 | if did_cache then return cache end 30 | cache = func() 31 | did_cache = true 32 | 33 | -- Let go of it for gc. 34 | ---@diagnostic disable-next-line cast-local-type 35 | func = nil 36 | return cache 37 | end 38 | end 39 | 40 | -- Given a value, return a function which returns the value. 41 | function mod.functionize(value) 42 | return function() 43 | return value 44 | end 45 | end 46 | 47 | return mod 48 | -------------------------------------------------------------------------------- /scripts/global-manager.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Functionality for managing global state, in particular splitting it up and 3 | typing it. 4 | 5 | The main entrypoint to this module is declare_global_module, called as: 6 | 7 | ``` 8 | local module_state = declare_global_module('rulers', {}, opts) 9 | ``` 10 | 11 | Where the second argument is either a table or a function taking a table key, 12 | which acts as a default value. Afterwords, `module_state[index]` invisibly and 13 | by default magically refers to `global.players[pindex].modulename`. Any index 14 | which is not present gets a copy of the default value, or if it is a function, 15 | whatever the function returns. 16 | 17 | If opts contains 'root_field', this will instead use global.root_field[index]. 18 | opts may be nil. 19 | 20 | You can do: 21 | 22 | ``` 23 | ---@class MyClass 24 | ---@field my_field String does cool stuff 25 | 26 | ---@type table 27 | local module_state = ... 28 | ``` 29 | 30 | Enabling both autocomplete and type checks. 31 | 32 | For testing, one may set persistent=false in opts. If so, the global state will 33 | be reset on first access. 34 | 35 | Some state such as that for the scanner is ephemeral. If this is the case, one 36 | may set ephemeral_state_version to an integer. When set for the first time or 37 | incremented, the state will be cleared before access for the first time the game 38 | starts with the mod updated. This is useful for example to let the scanner 39 | pickup backend changes, but comes at the cost of throwing out state. Use 40 | carefully. 41 | ]] 42 | 43 | local mod = {} 44 | 45 | ---@class fa.GlobalManagerOpts 46 | ---@field root_field string? 47 | ---@field persistent boolean? 48 | ---@field ephemeral_state_version number? 49 | 50 | ---@param module_name string 51 | ---@param default_value any 52 | ---@param opts? fa.GlobalManagerOpts 53 | ---@returns any 54 | function mod.declare_global_module(module_name, default_value, opts) 55 | assert(default_value ~= nil, "Default values of nil can't be put in a table as values") 56 | 57 | opts = opts or { 58 | root_field = "players", 59 | persistent = true, 60 | } 61 | 62 | local tried_clear = false 63 | 64 | local default_fn = default_value 65 | if type(default_fn) == "table" then 66 | default_fn = function(_key) 67 | return table.deepcopy(default_value) 68 | end 69 | elseif type(default_fn) ~= "function" then 70 | default_fn = function() 71 | return default_value 72 | end 73 | end 74 | 75 | local function do_clear_if_needed() 76 | if tried_clear then return end 77 | tried_clear = true 78 | 79 | local version_changed = false 80 | if opts.ephemeral_state_version then 81 | local ver_state = global.global_manager 82 | if not ver_state then 83 | ver_state = {} 84 | global.global_manager = {} 85 | end 86 | local ver_state = ver_state.versions 87 | if not ver_state then 88 | ver_state = {} 89 | global.global_manager.versions = ver_state 90 | end 91 | 92 | if not ver_state[opts.root_field] then ver_state[opts.root_field] = {} end 93 | local ver = ver_state[opts.root_field][module_name] 94 | version_changed = ver ~= opts.ephemeral_state_version 95 | ver_state[opts.root_field][module_name] = opts.ephemeral_state_version 96 | end 97 | 98 | if version_changed or opts.persistent == false then 99 | if not global[opts.root_field] then return end 100 | for k, e in pairs(global[opts.root_field]) do 101 | e[module_name] = nil 102 | end 103 | end 104 | end 105 | 106 | local meta = {} 107 | 108 | function meta:__newindex(index, nv) 109 | do_clear_if_needed() 110 | 111 | if not global[opts.root_field] then global[opts.root_field] = {} end 112 | if not global[opts.root_field][index] then global[opts.root_field][index] = {} end 113 | 114 | global[opts.root_field][index][module_name] = nv 115 | end 116 | 117 | function meta:__index(index) 118 | do_clear_if_needed() 119 | 120 | global[opts.root_field] = global[opts.root_field] or {} 121 | global[opts.root_field][index] = global[opts.root_field][index] or {} 122 | 123 | local possible = global[opts.root_field][index][module_name] 124 | 125 | if not possible then 126 | possible = default_fn(index) 127 | global[opts.root_field][index][module_name] = possible 128 | end 129 | 130 | -- Checked by the above assert and also LuaLS, but this isn't a critical 131 | -- path and it doesn't hurt. 132 | assert(possible, "Somehow, we got a default value of nil") 133 | 134 | return possible 135 | end 136 | 137 | local function wrapped_iter(is_ipairs) 138 | do_clear_if_needed() 139 | local last = nil 140 | local function ret() 141 | if not global[opts.root_field] then return nil end 142 | while true do 143 | last = next(global[opts.root_field], last) 144 | if not last then return nil end 145 | if global[opts.root_field][last][module_name] then 146 | if not is_ipairs or type(last) == "number" then 147 | return last, global[opts.root_field][last][module_name] 148 | end 149 | end 150 | end 151 | end 152 | 153 | return ret 154 | end 155 | 156 | function meta:__ipairs() 157 | return wrapped_iter(true) 158 | end 159 | function meta:__pairs() 160 | return wrapped_iter(false) 161 | end 162 | 163 | ret = {} 164 | setmetatable(ret, meta) 165 | return ret 166 | end 167 | 168 | return mod 169 | -------------------------------------------------------------------------------- /scripts/kruise-kontrol-wrapper.lua: -------------------------------------------------------------------------------- 1 | --Here: Functions related to Kruise Kontrol Remote 2 | 3 | local mod = {} 4 | 5 | local interface_name = "kruise_kontrol_updated" 6 | 7 | -- Call the closure if kk is present, returning what it returns. Otherwise don't 8 | -- call it and return `or_default`. Recall that unspecified parameters are 9 | -- already nil; `or_default`, therefore, is optional. 10 | local function call_with_interface(closure, or_default) 11 | if not remote.interfaces[interface_name] then return or_default end 12 | return closure() 13 | end 14 | 15 | --FA actions to take when KK activate input is pressed 16 | function mod.activate_kk(pindex) 17 | local announcing = call_with_interface(function() 18 | local p = game.get_player(pindex) 19 | -- If the player has no character then abort 20 | local c = p and p.valid and p.character 21 | if not c then return end 22 | -- The mod modifies this for e.g. telestep. 23 | p.character_running_speed_modifier = 0 24 | 25 | -- we want the mod's view of the cursor, which may be off the screen. 26 | -- 27 | -- Without the deep copy, control.lua touches this from under us. 28 | -- 29 | -- For now the fractional components are still present. We're about to 30 | -- fix that. 31 | local kk_pos = table.deepcopy(players[pindex].cursor_pos) 32 | 33 | -- we must duplicate a bit of logic since the mouse is not on our side; FA 34 | -- has its own idea of selections. 35 | refresh_player_tile(pindex) 36 | local target = get_first_ent_at_tile(pindex) 37 | 38 | -- Okay, but what other edge cases can we find? Turns out that, again, KK 39 | -- doesn't work if there's a blueprint in the player's hand. This one is 40 | -- really hard to resolve because some blueprints are and some blueprints 41 | -- aren't temporary, and mod hacking around blueprints is currently going 42 | -- on. For now, we will short-circuit and announce this to the player. 43 | -- 44 | -- Funnily enough deconstruction planners seem to be fine. As we find 45 | -- problems we can add them to the conditional below. 46 | 47 | local hand = p.cursor_stack 48 | if hand and hand.valid_for_read and (hand.name == "blueprint" or hand.name == "blueprint-book") then 49 | return { "fa.kk-blueprints-not-allowed" } 50 | end 51 | 52 | -- If in a car, make sure to activate it 53 | if p.vehicle and p.vehicle.type == "car" and p.vehicle.active == false then 54 | p.vehicle.active = true 55 | p.vehicle.speed = 0 56 | end 57 | 58 | -- Okay. Finally we're good. Let's kick this off. 59 | 60 | close_menu_resets(pindex) 61 | 62 | -- If cursor mode is on then the best case is that the mod announces a 63 | -- bunch of stuff it shouldn't, but sometimes this just flat out means 64 | -- that KK doesn't work. I don't know why; I'm guessing that's to do with 65 | -- how we hack WASD not to move the player. 66 | -- 67 | -- Don't say anything either, this is silent. 68 | force_cursor_off(pindex) 69 | 70 | ---@type table 71 | local opts = { x = math.floor(kk_pos.x), y = math.floor(kk_pos.y) } 72 | remote.call(interface_name, "start_job", pindex, opts, target) 73 | local desc = remote.call(interface_name, "get_description", pindex) 74 | if not desc then return { "fa.kk-not-started" } end 75 | 76 | return { "fa.kk-start", desc } 77 | end, { "fa.kk-not-available" }) 78 | 79 | printout(announcing, pindex) 80 | end 81 | 82 | --FA actions to take when KK cancel input is pressed 83 | function mod.cancel_kk(pindex) 84 | local p = game.get_player(pindex) 85 | call_with_interface(function() 86 | if not remote.call(interface_name, "is_active", pindex) then 87 | -- If there was no interface then KK isn't installed; if the player 88 | -- isn't active already then the enter key is doing enter/exit vehicle. 89 | -- In that case there's nothing to say here. 90 | return 91 | end 92 | 93 | remote.call(interface_name, "cancel", pindex) 94 | 95 | -- Prevent saying KK is done after it is cancelled. 96 | players[pindex].kruise_kontrol_active_last_time = false 97 | 98 | -- We screwed around with the running modifier. Put it back based on 99 | -- cursor mode. 100 | fix_walk(pindex) 101 | 102 | printout({ "fa.kk-cancel" }, pindex) 103 | end) 104 | -- If in a car, make sure to stop it because we are exiting it too because of the overlapping keys 105 | if p.vehicle and p.vehicle.type == "car" and p.vehicle.active == true then p.vehicle.speed = 0 end 106 | end 107 | 108 | function mod.status_read(pindex, short_version) 109 | call_with_interface(function() 110 | -- We must remember if KK was last active and then use it to detect the 111 | -- falling edge. This is the only way to really know if it's finished. 112 | local active = remote.call(interface_name, "is_active", pindex) 113 | local was_active = players[pindex].kruise_kontrol_active_last_time 114 | players[pindex].kruise_kontrol_active_last_time = active 115 | if active then 116 | printout({ "fa.kk-state", remote.call(interface_name, "get_description", pindex) }, pindex) 117 | elseif not active and was_active then 118 | printout({ "fa.kk-done" }, pindex) 119 | end 120 | end) 121 | end 122 | 123 | function mod.is_active(pindex) 124 | return call_with_interface(function() 125 | return remote.call(interface_name, "is_active", pindex) 126 | end, false) 127 | end 128 | 129 | return mod 130 | -------------------------------------------------------------------------------- /scripts/localising.lua: -------------------------------------------------------------------------------- 1 | --Here: localisation functions, including event handlers 2 | local mod = {} 3 | --Returns the localised name of an object as a string. Used for ents and items and fluids 4 | ---@return string 5 | function mod.get(object, pindex) 6 | -- Everything, everything uses this function without checking the return 7 | -- values. Use really annoying strings to make it very clear there's a 8 | -- bug. 9 | if pindex == nil then 10 | game.print("localising: pindex is nil error") 11 | return "NOT LOCALIZED!" 12 | end 13 | if object == nil then return "LOCALIZED OBJECT IS NIL!" end 14 | if object.valid and string.sub(object.object_name, -9) ~= "Prototype" then object = object.prototype end 15 | local result = players[pindex].localisations 16 | result = result and result[object.object_name] 17 | result = result and result[object.name] 18 | --for debugging 19 | if not result then 20 | game 21 | .get_player(pindex) 22 | .print("translation fallback for " .. object.object_name .. " " .. object.name, { volume_modifier = 0 }) 23 | end 24 | result = result or object.name 25 | return result 26 | end 27 | 28 | --Used for recipes 29 | function mod.get_alt(object, pindex) 30 | if pindex == nil then 31 | printout("localising: pindex is nil error") 32 | return "(nil)" 33 | end 34 | if object == nil then return "(nil)" end 35 | local result = players[pindex].localisations 36 | result = result and result[object.object_name] 37 | result = result and result[object.name] 38 | --for debugging 39 | if not result then 40 | game 41 | .get_player(pindex) 42 | .print("translation fallback for " .. object.object_name .. " " .. object.name, { volume_modifier = 0 }) 43 | end 44 | result = result or object.name 45 | return result or "(nil)" 46 | end 47 | 48 | function mod.get_item_from_name(name, pindex) 49 | local proto = game.item_prototypes[name] 50 | if proto == nil then return "(nil)" end 51 | local result = mod.get(proto, pindex) 52 | return result or "(nil)" 53 | end 54 | 55 | function mod.get_fluid_from_name(name, pindex) 56 | local proto = game.fluid_prototypes[name] 57 | if proto == nil then return "nil" end 58 | local result = mod.get(proto, pindex) 59 | return result 60 | end 61 | 62 | function mod.get_recipe_from_name(name, pindex) 63 | local proto = game.recipe_prototypes[name] 64 | if proto == nil then return "nil" end 65 | local result = mod.get_alt(proto, pindex) 66 | return result 67 | end 68 | 69 | function mod.get_item_group_from_name(name, pindex) 70 | local proto = game.item_group_prototypes[name] 71 | if proto == nil then return "nil" end 72 | local result = mod.get_alt(proto, pindex) 73 | return result 74 | end 75 | 76 | function mod.request_localisation(thing, pindex) 77 | local id = game.players[pindex].request_translation(thing.localised_name) 78 | local lookup = players[pindex].translation_id_lookup 79 | lookup[id] = { thing.object_name, thing.name } 80 | end 81 | 82 | function mod.request_all_the_translations(pindex) 83 | for _, cat in pairs({ 84 | "entity", 85 | "item", 86 | "fluid", 87 | "tile", 88 | "equipment", 89 | "damage", 90 | "virtual_signal", 91 | "recipe", 92 | "technology", 93 | "decorative", 94 | "autoplace_control", 95 | "mod_setting", 96 | "custom_input", 97 | "ammo_category", 98 | "item_group", 99 | "fuel_category", 100 | "achievement", 101 | "equipment_category", 102 | "shortcut", 103 | }) do 104 | for _, proto in pairs(game[cat .. "_prototypes"]) do 105 | mod.request_localisation(proto, pindex) 106 | end 107 | end 108 | end 109 | 110 | --Populates the appropriate localised string arrays for every translation 111 | function mod.handler(event) 112 | local pindex = event.player_index 113 | local player = players[pindex] 114 | local successful = event.translated 115 | local translated_thing = player.translation_id_lookup[event.id] 116 | if not translated_thing then return end 117 | player.translation_id_lookup[event.id] = nil 118 | if not successful then 119 | if player.translation_issue_counter == nil then 120 | player.translation_issue_counter = 1 121 | else 122 | player.translation_issue_counter = player.translation_issue_counter + 1 123 | end 124 | --print("translation request ".. event.id .. " failed, request: [" .. serpent.line(event.localised_string) .. "] for:" .. translated_thing[1] .. ":" .. translated_thing[2] .. ", total issues: " .. players[pindex].translation_issue_counter) 125 | return 126 | end 127 | if translated_thing == "test_translation" then 128 | local last_try = player.localisation_test 129 | if last_try == event.result then return end 130 | mod.request_all_the_translations(pindex) 131 | player.localisation_test = event.result 132 | return 133 | end 134 | player.localisations = player.localisations or {} 135 | local localised = player.localisations 136 | localised[translated_thing[1]] = localised[translated_thing[1]] or {} 137 | local translated_list = localised[translated_thing[1]] 138 | translated_list[translated_thing[2]] = event.result 139 | end 140 | 141 | function mod.check_player(pindex) 142 | local player = players[pindex] 143 | local id = game.players[pindex].request_translation({ "error.crash-to-desktop-message" }) 144 | if not id then return end 145 | player.translation_id_lookup = player.translation_id_lookup or {} 146 | player.translation_id_lookup[id] = "test_translation" 147 | end 148 | 149 | return mod 150 | -------------------------------------------------------------------------------- /scripts/memosort.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Table sorting with memoized scores. 3 | 4 | Normal sort in Lua takes function(a, b) ... end and computes the order in the 5 | function. That's fine for things which are cheap, e.g. just `a < b`, but once 6 | one starts doing math to figure it out it can become quite slow. 7 | 8 | Instead, we introduce a scoring function and a function memosort(). This is 9 | like table.sort(table, callback) but callback must return a number, representing 10 | the "score". For example, this might be the distance from the player. 11 | 12 | This interface guarantees exactly one call of the scoring callback per unique 13 | item, no more, no less. This is to facilitate in-place processing. For 14 | example, the scanner uses this to sort things which are groups of other things, 15 | while also sorting those other things. More specifically, we use this there to 16 | allow efficiently finding the closest item in a subcategory, while 17 | simultaneously sorting the subcategories. To be clearer, a call on an array 18 | like: 19 | 20 | ``` 21 | { 1, 2, 3, 3, 4, 4 } 22 | ``` 23 | 24 | Results only in 4 calls. 25 | 26 | A third optional argument may be used to use a cache across more than one 27 | memosort. This is useful if and only if there is commonality between the sorts. 28 | Such sorts must at minimum use the same exact scoring function. When this 29 | functionality is used, the scoring callback is *not* called on items which are 30 | in the cache from a previous sort (since doing so would defeat the purpose of 31 | sharing it). 32 | ]] 33 | local mod = {} 34 | 35 | ---@alias fa.memosort.ScoreCallback fun(any): number 36 | 37 | ---@param tab any[] 38 | ---@param callback fa.memosort.ScoreCallback 39 | ---@param cache table? 40 | function mod.memosort(tab, callback, cache) 41 | cache = cache or {} 42 | 43 | for i = 1, #tab do 44 | if cache[tab[i]] then goto continue end 45 | cache[tab[i]] = callback(tab[i]) 46 | 47 | ::continue:: 48 | end 49 | 50 | table.sort(tab, function(a, b) 51 | return cache[a] < cache[b] 52 | end) 53 | end 54 | 55 | return mod 56 | -------------------------------------------------------------------------------- /scripts/methods.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The ability to add methods to something, while keeping it safe to save in 3 | global. 4 | 5 | This is how one gets nice APIs like the Factorio entities, where one can 6 | `thing.whatever()`. Inheritance is not supported, and at least for now fields 7 | on the table must not have the same name as methods, or the field takes 8 | priority. 9 | 10 | This is a hard problem because Factorio cannot pass functions through global, 11 | nor can it restore metatables, unless those metatables are registerd during 12 | control.lua startup. 13 | 14 | This module exposes one function, `link`. It is called like: 15 | 16 | ``` 17 | -- A table of functions. They will be called as f(instance, user, supplied, arguments) 18 | -- 19 | -- They must be declared exactly once at module level. They cannot thus contain 20 | -- closures. 21 | local method_table = {} 22 | 23 | -- We can use the Lua method declaration syntax and self, to add methods to 24 | -- this table. 25 | function method_table:a_method(an_arg) 26 | -- Lua adds self for you, this is actually function(self, a_method). 27 | -- self is a valid variable here, referring to the "instance" (like Python). 28 | end 29 | 30 | -- Somewhere, at the top level, make this call, though probably 31 | -- with better naming: 32 | local linker = link_methods('a_unique_string', methods_table) 33 | 34 | -- And then to make a "instance", in a function or wherever. This is the magic. 35 | return linker(instance) 36 | ``` 37 | 38 | Where a_unique_string is unique for the lifetime of the *save*, and the method 39 | table is unique for the lifetime of this run. That is, the unique string 40 | "names" the methods, but the methods can change (e.g. adding new ones, 41 | renaming...). This doesn't care whether or not the code moves, or if it's in 42 | the module it was in, or anything--that string just has to be fixed. 43 | 44 | There are some minor catches: 45 | 46 | - The implementation uses a field _methods_private for itself. This is opaque; 47 | do not access it. 48 | - As above, no closures, and methods need to be registered at module top level. 49 | - And again as above, no fields with the same names as methods. 50 | - Also, there is a slight perf impact. It shouldn't matter, just don't use this 51 | in tight math loops. 52 | 53 | # Implementation 54 | 55 | This is actually just a metatable trick: register new metatables with factorio 56 | on declaration of methods, capturing the methods so declared. The wrapper then 57 | just installs the unique metatable. 58 | ]] 59 | 60 | local mod = {} 61 | 62 | -- This cache prevents us from having to make unique closures every time, by 63 | -- making the closure only once and then consulting it here. Keys are unique 64 | -- instances and values sets of bound methods. See the note in Lua 5.2's manual 65 | -- on ephemeron tables, in section 2.5.2. 66 | local bound_cache = {} 67 | setmetatable(bound_cache, { 68 | __mode = "k", 69 | }) 70 | 71 | local seenh_unique_names = {} 72 | 73 | -- This registers a metatable if and only if running in factorio. 74 | -- 75 | -- This is useful because we have self-tests near the end of this file, and 76 | -- those can be run from a shell. 77 | local function maybe_register(name, metatable) 78 | if script ~= nil and script.register_metatable ~= nil then script.register_metatable(name, metatable) end 79 | end 80 | 81 | function mod.link(unique_name, methods_table) 82 | if seenh_unique_names[unique_name] then error("Attempt to double-register " .. unique_name) end 83 | seenh_unique_names[unique_name] = true 84 | 85 | local meta_name = unique_name .. "-methods" 86 | 87 | local meta = { 88 | __index = function(table, key) 89 | if not methods_table[key] then return nil end 90 | 91 | local cached = bound_cache[table] 92 | if not cached then 93 | bound_cache[table] = {} 94 | cached = bound_cache[table] 95 | end 96 | 97 | local candidate = cached[key] 98 | if candidate then return candidate end 99 | 100 | -- Okay, fine, closure time. 101 | local closed = function(...) 102 | return methods_table[key](table, ...) 103 | end 104 | -- Since the top-level cache is ephemeron, Lua says it will drop the 105 | -- values. 106 | cached[key] = closed 107 | return closed 108 | end, 109 | } 110 | 111 | maybe_register(meta_name, meta) 112 | return function(instance) 113 | return setmetatable(instance, meta) 114 | end 115 | end 116 | 117 | -- These self-tests have to play with the gc to verify that the ephemeron table 118 | -- does what we want. 119 | if script == nil then 120 | print("methods.lua: outside factorio so Running self-tests...") 121 | -- a simple counter with inc and dec. 122 | local counter_methods = {} 123 | function counter_methods:inc(by) 124 | self.count = self.count + by 125 | end 126 | 127 | function counter_methods:dec(by) 128 | self.count = self.count - by 129 | end 130 | 131 | local linker = mod.link("methods_self_test", counter_methods) 132 | local instance = { count = 0 } 133 | linker(instance) 134 | 135 | instance.inc(5) 136 | assert(instance.count == 5) 137 | instance.dec(3) 138 | assert(instance.count == 2) 139 | -- Make sure this isn't an empty function; that was a bug during development. 140 | assert(instance.not_a_field == nil) 141 | 142 | -- there should currently be one key in our methods table before gc. 143 | local count = 0 144 | for k, v in pairs(bound_cache) do 145 | count = count + 1 146 | assert(k == instance) 147 | end 148 | assert(count == 1) 149 | 150 | -- Finally verify that ephemeron tables work how we expect. 151 | instance = nil 152 | collectgarbage() 153 | count = 0 154 | 155 | for k, v in pairs(bound_cache) do 156 | count = count + 1 157 | end 158 | 159 | assert(count == 0) 160 | end 161 | 162 | return mod 163 | -------------------------------------------------------------------------------- /scripts/mouse.lua: -------------------------------------------------------------------------------- 1 | --Here: Movement of the mouse pointer on screen 2 | --Note: Does not include the mod cursor functions! 3 | 4 | local fa_utils = require("scripts.fa-utils") 5 | local dirs = defines.direction 6 | local mod = {} 7 | 8 | --Moves the mouse pointer to the correct pixel on the screen for an input map position. If the position is off screen, then the pointer is centered on the player character instead. Does not run in vanilla mode or if the mouse is released from synchronizing. 9 | function mod.move_mouse_pointer(position, pindex) 10 | local player = players[pindex] 11 | local pos = position 12 | local screen = game.players[pindex].display_resolution 13 | local screen_center = fa_utils.mult_position({ x = screen.width, y = screen.height }, 0.5) 14 | local pixels = screen_center 15 | local offset = { x = 0, y = 0 } 16 | if players[pindex].vanilla_mode or game.get_player(pindex).game_view_settings.update_entity_selection == true then 17 | return 18 | elseif player.remote_view == true then 19 | --If in remote view, take the cursor position as the center point 20 | offset = fa_utils.mult_position(fa_utils.sub_position(pos, player.cursor_pos), 32 * player.zoom) 21 | elseif mod.cursor_position_is_on_screen_with_player_centered(pindex) == false then 22 | --If the cursor is distant, center the pointer on the player 23 | pos = players[pindex].position 24 | offset = fa_utils.mult_position(fa_utils.sub_position(pos, player.position), 32 * player.zoom) 25 | else 26 | offset = fa_utils.mult_position(fa_utils.sub_position(pos, player.position), 32 * player.zoom) 27 | end 28 | pixels = fa_utils.add_position(pixels, offset) 29 | mod.move_pointer_to_pixels(pixels.x, pixels.y, pindex) 30 | --game.get_player(pindex).print("moved to " .. math.floor(pixels.x) .. " , " .. math.floor(pixels.y), {volume_modifier=0})-- 31 | end 32 | 33 | --Moves the mouse pointer to specified pixels on the screen. 34 | function mod.move_pointer_to_pixels(x, y, pindex) 35 | if 36 | x >= 0 37 | and y >= 0 38 | and x < game.players[pindex].display_resolution.width 39 | and y < game.players[pindex].display_resolution.height 40 | then 41 | print("setCursor " .. pindex .. " " .. math.ceil(x) .. "," .. math.ceil(y)) 42 | end 43 | end 44 | 45 | --Checks if the map position of the mod cursor falls on screen when the camera is locked on the player character. 46 | function mod.cursor_position_is_on_screen_with_player_centered(pindex) 47 | local range_y = math.floor(18 / players[pindex].zoom) --found experimentally by counting tile ranges at different zoom levels 48 | local range_x = range_y * game.get_player(pindex).display_scale * 1.6 --found experimentally by checking scales 49 | return ( 50 | math.abs(players[pindex].cursor_pos.y - players[pindex].position.y) <= range_y 51 | and math.abs(players[pindex].cursor_pos.x - players[pindex].position.x) <= range_x 52 | ) 53 | end 54 | 55 | --Reports if the cursor tile is uncharted/blurred and also if it is distant (off screen) 56 | function mod.cursor_visibility_info(pindex) 57 | local p = game.get_player(pindex) 58 | local result = "" 59 | local pos = players[pindex].cursor_pos 60 | local chunk_pos = { x = math.floor(pos.x / 32), y = math.floor(pos.y / 32) } 61 | if p.force.is_chunk_charted(p.surface, chunk_pos) == false then 62 | result = result .. " uncharted " 63 | elseif p.force.is_chunk_visible(p.surface, chunk_pos) == false then 64 | result = result .. " blurred " 65 | end 66 | if mod.cursor_position_is_on_screen_with_player_centered(pindex) == false then result = result .. " distant " end 67 | return result 68 | end 69 | 70 | return mod 71 | -------------------------------------------------------------------------------- /scripts/quickbar.lua: -------------------------------------------------------------------------------- 1 | --Here: Quickbar related functions 2 | local fa_localising = require("scripts.localising") 3 | 4 | local mod = {} 5 | 6 | ---@param event EventData.CustomInputEvent 7 | function mod.quickbar_get_handler(event) 8 | pindex = event.player_index 9 | if not check_for_player(pindex) then return end 10 | if 11 | players[pindex].menu == "inventory" 12 | or players[pindex].menu == "none" 13 | or (players[pindex].menu == "building" or players[pindex].menu == "vehicle") 14 | then 15 | local num = tonumber(string.sub(event.input_name, -1)) 16 | if num == 0 then num = 10 end 17 | mod.read_quick_bar_slot(num, pindex) 18 | end 19 | end 20 | 21 | --all 10 quickbar slot setting event handlers 22 | ---@param event EventData.CustomInputEvent 23 | function mod.quickbar_set_handler(event) 24 | pindex = event.player_index 25 | if not check_for_player(pindex) then return end 26 | if 27 | players[pindex].menu == "inventory" 28 | or players[pindex].menu == "none" 29 | or (players[pindex].menu == "building" or players[pindex].menu == "vehicle") 30 | then 31 | local num = tonumber(string.sub(event.input_name, -1)) 32 | if num == 0 then num = 10 end 33 | mod.set_quick_bar_slot(num, pindex) 34 | end 35 | end 36 | 37 | --all 10 quickbar page setting event handlers 38 | ---@param event EventData.CustomInputEvent 39 | function mod.quickbar_page_handler(event) 40 | pindex = event.player_index 41 | if not check_for_player(pindex) then return end 42 | 43 | local num = tonumber(string.sub(event.input_name, -1)) 44 | if num == 0 then num = 10 end 45 | mod.read_switched_quick_bar(num, pindex) 46 | end 47 | 48 | function mod.read_quick_bar_slot(index, pindex) 49 | page = game.get_player(pindex).get_active_quick_bar_page(1) - 1 50 | local item = game.get_player(pindex).get_quick_bar_slot(index + 10 * page) 51 | if item ~= nil then 52 | local count = game.get_player(pindex).get_main_inventory().get_item_count(item.name) 53 | local stack = game.get_player(pindex).cursor_stack 54 | if stack and stack.valid_for_read then 55 | count = count + stack.count 56 | printout("unselected " .. fa_localising.get(item, pindex) .. " x " .. count, pindex) 57 | else 58 | printout("selected " .. fa_localising.get(item, pindex) .. " x " .. count, pindex) 59 | end 60 | else 61 | printout("Empty quickbar slot", pindex) --does this print, maybe not working because it is linked to the game control? 62 | end 63 | end 64 | 65 | function mod.set_quick_bar_slot(index, pindex) 66 | local p = game.get_player(pindex) 67 | local page = game.get_player(pindex).get_active_quick_bar_page(1) - 1 68 | local stack_cur = game.get_player(pindex).cursor_stack 69 | local stack_inv = players[pindex].inventory.lua_inventory[players[pindex].inventory.index] 70 | local ent = p.selected 71 | if stack_cur and stack_cur.valid_for_read and stack_cur.valid == true then 72 | game.get_player(pindex).set_quick_bar_slot(index + 10 * page, stack_cur) 73 | printout("Quickbar assigned " .. index .. " " .. fa_localising.get(stack_cur, pindex), pindex) 74 | elseif 75 | players[pindex].menu == "inventory" 76 | and stack_inv 77 | and stack_inv.valid_for_read 78 | and stack_inv.valid == true 79 | then 80 | game.get_player(pindex).set_quick_bar_slot(index + 10 * page, stack_inv) 81 | printout("Quickbar assigned " .. index .. " " .. fa_localising.get(stack_inv, pindex), pindex) 82 | elseif ent ~= nil and ent.valid and ent.force == p.force and game.item_prototypes[ent.name] ~= nil then 83 | game.get_player(pindex).set_quick_bar_slot(index + 10 * page, ent.name) 84 | printout("Quickbar assigned " .. index .. " " .. fa_localising.get(ent, pindex), pindex) 85 | else 86 | --Clear the slot 87 | local item = game.get_player(pindex).get_quick_bar_slot(index + 10 * page) 88 | local item_name = "" 89 | if item ~= nil then item_name = fa_localising.get(item, pindex) end 90 | ---@diagnostic disable-next-line: param-type-mismatch 91 | game.get_player(pindex).set_quick_bar_slot(index + 10 * page, nil) 92 | printout("Quickbar unassigned " .. index .. " " .. item_name, pindex) 93 | end 94 | end 95 | 96 | function mod.read_switched_quick_bar(index, pindex) 97 | page = game.get_player(pindex).get_active_quick_bar_page(index) 98 | local item = game.get_player(pindex).get_quick_bar_slot(1 + 10 * (index - 1)) 99 | local item_name = "empty slot" 100 | if item ~= nil then item_name = fa_localising.get(item, pindex) end 101 | local result = "Quickbar " .. index .. " selected starting with " .. item_name 102 | printout(result, pindex) 103 | end 104 | 105 | return mod 106 | -------------------------------------------------------------------------------- /scripts/scanner/backends/simple.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | A simple backend. 3 | 4 | This backend handles a vast majority of cases, all of which delegate to fa-info 5 | for reading and fa-utils to find the top-left corner. This handles almost 6 | everything. Rails is a special case, as for curved-rail we need the center; to 7 | deal with that, this code just hardcodes it in. By doing so we match what the 8 | cursor would say. 9 | 10 | The one thing this does not know about is category, so one must call 11 | declare_simple_backend with a (sub)category set of callbacks. Default is other, 12 | providing a way to see un-categorized things, and prototype name. For future 13 | proofing, it is also possible to customize readouts. 14 | 15 | Unfortunately, we get a notable performance increase if we cache entries. So 16 | there is that as well. We also get a large jump if we inline table constants 17 | when dumping to callbacks. So this is a bit ugly, but it's ugly because 18 | performance--a midgame save goes down from 80ms to under 40ms with respect to 19 | scanning everything in the logistics category (belts, etc), for instance. Note 20 | that it is crutial to update positions and subcategories when dumping, so even 21 | when pulling from the cache we must still do those fields. The others can be 22 | brought forward on updates. 23 | 24 | For now, this assumes category cannot change. 25 | 26 | fa-utils corner finding relies on map queries and is slow. As a result, the 27 | entry is put into the cache using an approximated top left corner and then that 28 | is made more accurate in update_entry. The problem we face is that some 29 | entities, primarily rocket ship pieces in the initial crash site, don't quite 30 | line up with tiles. A longer term solution is to use the bounding boxes. 31 | Unfortunately however, we aren't set up to do that safely until we have a better 32 | cursor handling solution because the points such boxes can return are by their 33 | nature highly irregular. Since we can do this on a slow path and since we know 34 | that the old scanner used that successfully, we just do it. 35 | ]] 36 | local FaInfo = require("scripts.fa-info") 37 | local FaUtils = require("scripts.fa-utils") 38 | local ScannerConsts = require("scripts.scanner.scanner-consts") 39 | local TH = require("scripts.table-helpers") 40 | 41 | local mod = {} 42 | 43 | local function default_category_cb(ent) 44 | return ScannerConsts.CATEGORIES.OTHER 45 | end 46 | 47 | local function default_subcategory_cb(ent) 48 | return ent.name 49 | end 50 | 51 | local function default_readout_cb(player, ent) 52 | return FaInfo.ent_info(player.index, ent, nil, true) 53 | end 54 | 55 | ---@class fa.scanner.backends.SimpleBackend: fa.scanner.ScannerBackend 56 | ---@field known_entities table 57 | ---@field entry_cache table 58 | ---@field readout_callback fun(LuaPlayer, LuaEntity): LocalisedString 59 | ---@field category_callback(LuaEntity): fa.scanner.Category 60 | ---@field subcategory_callback fun(LuaEntity): fa.scanner.Subcategory 61 | 62 | local SimpleBackend = {} 63 | 64 | ---@param e fa.scanner.ScanEntry 65 | function SimpleBackend:validate_entry(player, e) 66 | return e.backend_data.valid and player.surface_index == e.backend_data.surface_index 67 | end 68 | 69 | function SimpleBackend:update_entry(_player, entry) 70 | local entity = entry.backend_data 71 | if entity.type == "curved-rail" then 72 | -- curved-rail special case: take the center. 73 | entry.position = entity.position 74 | else 75 | entry.position = FaUtils.get_ent_northwest_corner_position(entity) 76 | end 77 | entry.backend = self 78 | entry.backend_data = entity 79 | entry.category = self.category_callback(entity) 80 | entry.subcategory = self.subcategory_callback(entity) 81 | end 82 | 83 | function SimpleBackend:readout_entry(player, e) 84 | return self.readout_callback(player, e.backend_data) 85 | end 86 | 87 | function SimpleBackend:on_new_entity(ent) 88 | if not ent.valid then return end 89 | 90 | self.known_entities[script.register_on_entity_destroyed(ent)] = ent 91 | end 92 | 93 | ---@param event EventData.on_entity_destroyed 94 | function SimpleBackend:on_entity_destroyed(event) 95 | self.known_entities[event.registration_number] = nil 96 | self.entry_cache[event.registration_number] = nil 97 | end 98 | 99 | function SimpleBackend:dump_entries_to_callback(player, callback) 100 | local cat_cb = self.category_callback 101 | local subcat_cb = self.subcategory_callback 102 | 103 | for regnum, entity in pairs(self.known_entities) do 104 | if not entity.valid then 105 | self.known_entities[regnum] = nil 106 | self.entry_cache[regnum] = nil 107 | else 108 | -- Lua optimizes the case of being able to pre-lookup functions into 109 | --locals, as well as the case wherein one writes out a table as a 110 | --constant (it knows how to allocate exactly the right sizes in that 111 | --case). This got us 30% gains. This means that the following cannot 112 | --be factored out into helper functions easily, if at all. 113 | local pos = entity.position 114 | local x, y = pos.x, pos.y 115 | local tw, th = entity.tile_width, entity.tile_height 116 | local htw = tw / 2 117 | local hth = th / 2 118 | 119 | local effective_x, effective_y 120 | 121 | if entity.type == "curved-rail" then 122 | -- curved-rail special case: take the center. 123 | effective_x = x 124 | effective_y = y 125 | else 126 | -- This is accurate and better than going through FaUtils in terms of 127 | -- performance since that does map queries. We will probably fix FaUtils 128 | -- but for now we limit the fallout to here. 129 | effective_x = x - htw 130 | effective_y = y - hth 131 | end 132 | 133 | local cached_entry = self.entry_cache[regnum] 134 | if cached_entry then 135 | cached_entry.position = { x = effective_x, y = effective_y } 136 | cached_entry.subcategory = subcat_cb(entity) 137 | callback(cached_entry) 138 | else 139 | local entry = { 140 | position = { x = effective_x, y = effective_y }, 141 | category = cat_cb(entity), 142 | subcategory = subcat_cb(entity), 143 | backend = self, 144 | backend_data = entity, 145 | } 146 | self.entry_cache[regnum] = entry 147 | callback(entry) 148 | end 149 | end 150 | end 151 | end 152 | 153 | function SimpleBackend:on_new_tiles(tiles) end 154 | 155 | ---@class fa.scanner.SimpleBackendCallbacks 156 | ---@field category_callback (fun(e: LuaEntity):fa.scanner.Category)? 157 | ---@field subcategory_callback (fun(LuaEntity): fa.scanner.Subcategory)? 158 | ---@field readout_callback (fun(LuaPlayer, LuaEntity): LocalisedString)? 159 | 160 | ---@param callbacks fa.scanner.SimpleBackendCallbacks 161 | ---@return fa.scanner.ScannerBackend 162 | function mod.declare_simple_backend(meta_name, callbacks) 163 | local callbacks_defaulted = { 164 | category_callback = callbacks.category_callback or default_category_cb, 165 | subcategory_callback = callbacks.subcategory_callback or default_subcategory_cb, 166 | readout_callback = callbacks.readout_callback or default_readout_cb, 167 | } 168 | 169 | local newmeta = TH.nested_indexer(SimpleBackend, callbacks_defaulted) 170 | 171 | if script then script.register_metatable(meta_name, newmeta) end 172 | 173 | local ret = { 174 | new = function() 175 | local r = { 176 | known_entities = {}, 177 | entry_cache = {}, 178 | } 179 | setmetatable(r, newmeta) 180 | return r 181 | end, 182 | } 183 | 184 | return ret 185 | end 186 | 187 | function SimpleBackend:get_aabb(e) 188 | local aabb = e.backend_data.bounding_box 189 | local lt = aabb.left_top 190 | local rb = aabb.right_bottom 191 | return lt.x, lt.y, rb.x, rb.y 192 | end 193 | 194 | function SimpleBackend:is_huge(e) 195 | return false 196 | end 197 | 198 | return mod 199 | -------------------------------------------------------------------------------- /scripts/scanner/backends/single-entity.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | We have to declare categorization and subcategorization functions for everything 3 | by declaring our backends. Simple already does everything else for us. 4 | ]] 5 | local BuildingTools = require("scripts.building-tools") 6 | local decl = require("scripts.scanner.backends.simple").declare_simple_backend 7 | local functionize = require("scripts.functools").functionize 8 | local Info = require("scripts.fa-info") 9 | local SC = require("scripts.scanner.scanner-consts") 10 | 11 | local mod = {} 12 | 13 | -- For things such as crafting machines, trains, etc. the category is 14 | -- 'prototype/recipe', 'prototype/train-name', etc. 15 | function cat2(c1, c2) 16 | return string.format("%s/%s", c1, c2) 17 | end 18 | 19 | -- Quickly declare backends where it's just fixing the category to something not 20 | -- default. 21 | local function decl_bound_category(metaname, category) 22 | return decl(metaname, { 23 | category_callback = functionize(category), 24 | }) 25 | end 26 | 27 | mod.CraftingMachine = decl("fa.scanner.backends.CraftingMachine", { 28 | category_callback = functionize(SC.CATEGORIES.PRODUCTION), 29 | ---@param ent LuaEntity 30 | subcategory_callback = function(ent) 31 | local r = ent.get_recipe() 32 | local rn = r and r.name or "" 33 | return cat2(ent.name, rn) 34 | end, 35 | }) 36 | 37 | mod.MiningDrill = decl("fa.scanner.backends.MiningDrill", { 38 | category_callback = functionize(SC.CATEGORIES.PRODUCTION), 39 | subcategory_callback = function(ent) 40 | local under_drill = Info.compute_resources_under_drill(ent) 41 | local keys = {} 42 | for k in pairs(under_drill) do 43 | table.insert(keys, k) 44 | end 45 | table.sort(keys) 46 | local key_part = table.concat(keys, "/") 47 | return cat2(ent.name, key_part) 48 | end, 49 | }) 50 | 51 | mod.Furnace = decl("fa.scanner.backends.Furnace", { 52 | category_callback = functionize(SC.CATEGORIES.PRODUCTION), 53 | ---@param ent LuaEntity 54 | subcategory_callback = function(ent) 55 | local recipe = ent.get_recipe() 56 | local rname = recipe and recipe.name or nil 57 | 58 | local oi = ent.get_output_inventory() 59 | if not rname and #oi > 0 and oi[1].valid_for_read then rname = oi[1].name end 60 | return cat2(ent.name, rname or "") 61 | end, 62 | }) 63 | 64 | mod.Vehicle = decl_bound_category("fa.scanner.backends.Vehicle", SC.CATEGORIES.VEHICLES) 65 | 66 | -- rail, curved-rail, signals are all "boring". Stops and cars are more 67 | -- complicated. 68 | mod.TrainsSimple = decl_bound_category("fa.scanner.backends.TrainsSimple", SC.CATEGORIES.TRAINS) 69 | 70 | -- For trains, we group by the train id only, so that the scanner isn't super 71 | -- cluttered with random train cars. In the future, we probably want a custom 72 | -- backend. 73 | mod.TrainsNamed = decl("fa.scanner.backends.TrainsNamed", { 74 | category_callback = functionize(SC.CATEGORIES.TRAINS), 75 | ---@param ent LuaEntity 76 | subcategory_callback = function(ent) 77 | return cat2("train", tostring(ent.train.id)) 78 | end, 79 | }) 80 | 81 | mod.Ghosts = decl("fa.scanner.backends.Ghosts", { 82 | category_callback = functionize(SC.CATEGORIES.GHOSTS), 83 | ---@param ent LuaEntity 84 | subcategory_callback = function(ent) 85 | return ent.ghost_type 86 | end, 87 | }) 88 | 89 | mod.Character = decl_bound_category("fa.scanner.backends.Character", SC.CATEGORIES.PLAYERS) 90 | 91 | -- Unit are enemies in vanilla. 92 | mod.Unit = decl_bound_category("fa.scanner.backends.Unit", SC.CATEGORIES.ENEMIES) 93 | 94 | -- Spawners need to be grouped by pollution. Buckets copied from old scanner. 95 | local SPAWNER_POLLUTION_BUCKETS = { 96 | { 0, { "fa.scanner-spawner-polluted-none" } }, 97 | { 1, { "fa.scanner-spawner-polluted-lightly" } }, 98 | { 99, { "fa.scanner-spawner-polluted-heavily" } }, 99 | } 100 | 101 | mod.Spawner = decl("fa.scanner.backends.Spawner", { 102 | category_callback = functionize(SC.CATEGORIES.ENEMIES), 103 | 104 | ---@param ent LuaEntity 105 | subcategory_callback = function(ent) 106 | local level = 0 107 | local p = ent.absorbed_pollution 108 | for i = 1, #SPAWNER_POLLUTION_BUCKETS do 109 | if SPAWNER_POLLUTION_BUCKETS[i][1] <= p then 110 | level = i 111 | else 112 | break 113 | end 114 | end 115 | 116 | return cat2(ent.name, tostring(level)) 117 | end, 118 | 119 | ---@param ent LuaEntity 120 | readout_callback = function(player, ent) 121 | local result = SPAWNER_POLLUTION_BUCKETS[1][2] 122 | local p = ent.absorbed_pollution 123 | for i = 1, #SPAWNER_POLLUTION_BUCKETS do 124 | if SPAWNER_POLLUTION_BUCKETS[i][1] <= p then 125 | result = SPAWNER_POLLUTION_BUCKETS[i][2] 126 | else 127 | break 128 | end 129 | end 130 | 131 | local info_string = Info.ent_info(player.index, ent, nil, true) 132 | return { "fa.scanner-spawner-announce", info_string, result } 133 | end, 134 | }) 135 | 136 | -- There are so many logistics items that we will make one generic backend and 137 | -- list them off in the LUT instead (inserters, transport belts, splitters, so 138 | -- on). 139 | mod.LogisticsAndPower = decl_bound_category("fa.scanner.backends.LogisticsAndPower", SC.CATEGORIES.LOGISTICSAndPower) 140 | mod.Production = decl_bound_category("fa.scanner.backends.Production", SC.CATEGORIES.PRODUCTION) 141 | mod.Military = decl_bound_category("fa.scanner.backends.Military", SC.CATEGORIES.MILITARY) 142 | mod.Other = decl_bound_category("fa.scanner.backends.Other", SC.CATEGORIES.OTHER) 143 | mod.Remnants = decl_bound_category("fa.scanner.backends.Remnants", SC.CATEGORIES.REMNANTS) 144 | 145 | mod.Containers = decl("fa.scanner.backends.Containers", { 146 | category_callback = functionize(SC.CATEGORIES.CONTAINERS), 147 | ---@param ent LuaEntity 148 | subcategory_callback = function(ent) 149 | local itemset = ent.get_inventory(defines.inventory.chest).get_contents() 150 | local subcat 151 | -- This is a set not an array, and we care if it has 0, 1, or multiple 152 | -- items. To do that, pull out the first two keys. 153 | local key1 = next(itemset) 154 | local key2 = next(itemset, key1) 155 | local subcat 156 | if key1 and not key2 then 157 | subcat = key1 158 | elseif not key1 then 159 | subcat = "" 160 | else 161 | subcat = "" 162 | end 163 | return cat2(ent.name, subcat) 164 | end, 165 | }) 166 | 167 | mod.Corpses = decl_bound_category("fa.scanner.backends.Corpses", SC.CATEGORIES.CORPSES) 168 | 169 | -- For rocks. 170 | mod.Rock = decl_bound_category("fa.scanner.backends.ResourceSingle", SC.CATEGORIES.RESOURCES) 171 | 172 | -- When used on something with a fluidbox, group by the contained fluid. In the 173 | -- rare case of multiple fluids, will group by one arbitrarily, not necessarily 174 | -- the same one each time. 175 | mod.LogisticsWithFluid = decl("fa.scanner.backends.LogisticsWithFluid", { 176 | category_callback = functionize(SC.CATEGORIES.LOGISTICSAndPower), 177 | 178 | ---@param ent LuaEntity 179 | subcategory_callback = function(ent) 180 | local fluids = ent.get_fluid_contents() 181 | local fluid_name = next(fluids) or "" 182 | return cat2(ent.name, fluid_name) 183 | end, 184 | }) 185 | 186 | -- Roboports are categorized by network name. 187 | mod.Roboport = decl("fa.scanner.backends.Roboport", { 188 | category_callback = functionize(SC.CATEGORIES.LOGISTICSAndPower), 189 | 190 | ---@param ent LuaEntity 191 | subcategory_callback = function(ent) 192 | return cat2(ent.name, ent.backer_name) 193 | end, 194 | }) 195 | 196 | mod.Pipe = decl("fa.scanner.backends.Pipe", { 197 | category_callback = functionize(SC.CATEGORIES.LOGISTICSAndPower), 198 | 199 | ---@param ent LuaEntity 200 | subcategory_callback = function(ent) 201 | local fluids = ent.get_fluid_contents() 202 | local fluid = next(fluids) or "" 203 | local end_part = BuildingTools.is_a_pipe_end(ent) and "" or "" 204 | return string.format("%s/%s/%s", ent.name, fluid, end_part) 205 | end, 206 | }) 207 | return mod 208 | -------------------------------------------------------------------------------- /scripts/scanner/backends/trees.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Trees have the same problem as water: there's a billion of them spread widely. 3 | 4 | Blind players can't really tell how "perfectt" this is. So, two basic things: 5 | 6 | - The foreests are announced as forests if they're far away. 7 | - The forests are announced as trees if they're close. 8 | 9 | And then to make this work we divide them into tiles like how the game does 10 | chunks, and use the much faster tile clusterer. Any chunk in a forest close 11 | enough to the player gets converted to trees. 12 | 13 | It's good enough as long as two things hold: the cursor ends up on a tree and 14 | the player can see trees close to them. 15 | 16 | the one thing that does come up here is that it's important for us to be able to 17 | cluster in batches. We thus do the clustering when dumping, but the work which 18 | is done at that time is saved. 19 | ]] 20 | local Info = require("scripts.fa-info") 21 | local SC = require("scripts.scanner.scanner-consts") 22 | local TileClusterer = require("scripts.ds.tile-clusterer") 23 | local TH = require("scripts.table-helpers") 24 | 25 | local mod = {} 26 | 27 | ---@class fa.scanner.backends.TreeBackend: fa.scanner.ScannerBackend 28 | ---@field point_queue { x: number, y: number }[] 29 | ---@field clusterer fa.ds.TileClusterer 30 | ---@field chunk_size number Saved version of ScannerConsts's value. 31 | ---@field surface LuaSurface 32 | local TreeBackend = {} 33 | local TreeBackend_meta = { __index = TreeBackend } 34 | if script then script.register_metatable("fa.scanner.backends.TreeBackenmd", TreeBackend_meta) end 35 | mod.TreeBackend = TreeBackend 36 | 37 | ---@return fa.scanner.backends.TreeBackend 38 | function TreeBackend.new(surface) 39 | ---@type fa.scanner.backends.TreeBackend 40 | local r = { 41 | clusterer = TileClusterer.TileClusterer.new({ track_interior = true }), 42 | chunk_size = SC.FOREST_CHUNK_SIZE, 43 | point_queue = {}, 44 | surface = surface, 45 | } 46 | 47 | return setmetatable(r, TreeBackend_meta) 48 | end 49 | 50 | function TreeBackend:on_new_entity(entity) 51 | local cx, cy = math.floor(entity.position.x / self.chunk_size), math.floor(entity.position.y / self.chunk_size) 52 | 53 | table.insert(self.point_queue, { x = cx, y = cy }) 54 | end 55 | 56 | ---@param event EventData.on_entity_destroyed 57 | function TreeBackend:on_entity_destroyed(event) end 58 | 59 | function TreeBackend:update_entry(player, e) 60 | local aabb = e.backend_data.aabb 61 | local trees = self.surface.find_entities_filtered({ area = aabb, type = "tree" }) 62 | local closest = self.surface.get_closest(player.position, trees) 63 | -- It's still in the AABB, just a different point. 64 | e.position = closest.position 65 | end 66 | 67 | function TreeBackend:validate_entry(player, e) 68 | return self.surface.valid 69 | and self.surface.count_entities_filtered({ area = e.backend_data.aabb, type = "tree", limit = 1 }) > 0 70 | end 71 | 72 | ---@param player LuaPlayer 73 | function TreeBackend:readout_entry(player, e) 74 | if e.backend_data.zoom_ent then 75 | return Info.ent_info(player.index, e.backend_data.zoom_ent, nil, true) 76 | else 77 | return { "fa.scanner-forest", e.backend_data.tree_count } 78 | end 79 | end 80 | 81 | ---@param player LuaPlayer 82 | ---@param callback fun(fa.scanner.ScanEntry) 83 | function TreeBackend:dump_entries_to_callback(player, callback) 84 | -- Step 1: feed all of OUR NEW POINTS To THE CLUSTERER. 85 | if next(self.point_queue) then 86 | self.clusterer:submit_points(self.point_queue) 87 | self.point_queue = {} 88 | end 89 | local px, py = player.position.x, player.position.y 90 | local CAT_RESOURCES = SC.CATEGORIES.RESOURCES 91 | local find_entities_filtered = self.surface.find_entities_filtered 92 | 93 | ---@param g fa.ds.TileClusterer.Group 94 | self.clusterer:get_groups(function(g) 95 | -- We work out the count, bounding box, and then find a tree closest to 96 | -- the player. 97 | local tlx = math.huge 98 | local tly = math.huge 99 | local brx = -math.huge 100 | local bry = -math.huge 101 | 102 | for x, children in pairs(g.edge_tiles) do 103 | tlx = tlx < x and tlx or x 104 | brx = brx > x and brx or x 105 | 106 | for y in pairs(children) do 107 | tly = tly < y and tly or y 108 | bry = bry > y and bry or y 109 | end 110 | end 111 | 112 | -- These are the top left of a tile; make them the bottom right. 113 | brx = brx + 1 114 | bry = bry + 1 115 | 116 | tlx = tlx * self.chunk_size 117 | brx = brx * self.chunk_size 118 | tly = tly * self.chunk_size 119 | bry = bry * self.chunk_size 120 | 121 | local aabb = { 122 | left_top = { x = tlx, y = tly }, 123 | right_bottom = { x = brx, y = bry }, 124 | } 125 | 126 | local trees = find_entities_filtered({ area = aabb, type = "tree" }) 127 | -- If still not, skip this one. 128 | if not next(trees) then return end 129 | 130 | local zoomed_trees = {} 131 | 132 | -- Filter out any trees close enough for zoom. 133 | local d_squared = SC.FOREST_ZOOM_DISTANCE ^ 2 134 | TH.retain_unordered(trees, function(t) 135 | local zoomed = (player.position.x - t.position.x) ^ 2 + (player.position.y - t.position.y) ^ 2 < d_squared 136 | if zoomed then table.insert(zoomed_trees, t) end 137 | return not zoomed 138 | end) 139 | 140 | -- Figure out the big non-zoomed one. 141 | if next(trees) then 142 | local closest = self.surface.get_closest(player.position, trees) 143 | 144 | -- If there is only one tree, don't announce as trees x1. instead 145 | -- "zoom" into it. 146 | local zoom_ent = #trees == 1 and trees[1] or nil 147 | if zoom_ent then aabb = trees[1].bounding_box end 148 | callback({ 149 | backend = self, 150 | backend_data = { tree_count = #trees, aabb = aabb, zoom_ent = zoom_ent }, 151 | position = closest.position, 152 | category = CAT_RESOURCES, 153 | subcategory = "tree", 154 | }) 155 | end 156 | 157 | for _, t in pairs(zoomed_trees) do 158 | callback({ 159 | position = t.position, 160 | category = CAT_RESOURCES, 161 | subcategory = "tree", 162 | backend = self, 163 | backend_data = { tree_count = 1, zoom_ent = t, aabb = t.bounding_box }, 164 | }) 165 | end 166 | end) 167 | end 168 | 169 | function TreeBackend:get_aabb(e) 170 | local aabb = e.backend_data.aabb 171 | local lt = aabb.left_top 172 | local rb = aabb.right_bottom 173 | return lt.x, lt.y, rb.x, rb.y 174 | end 175 | 176 | function TreeBackend:is_huge(e) 177 | return e.backend_data.tree_count > 100 178 | end 179 | 180 | return mod 181 | -------------------------------------------------------------------------------- /scripts/scanner/backends/water.lua: -------------------------------------------------------------------------------- 1 | local Clusterer = require("scripts.ds.clusterer") 2 | local SC = require("scripts.scanner.scanner-consts") 3 | local TH = require("scripts.table-helpers") 4 | local Memosort = require("scripts.memosort") 5 | local TileClusterer = require("scripts.ds.tile-clusterer") 6 | 7 | local mod = {} 8 | 9 | local WATER_PROTOS_SET = {} 10 | TH.array_to_set(WATER_PROTOS_SET, SC.WATER_PROTOS) 11 | 12 | ---@class fa.scanner.backends.WaterBackendData 13 | ---@field aabb fa.AABB 14 | 15 | ---@class fa.scanner.WaterBackend: fa.scanner.ScannerBackend 16 | ---@field surface LuaSurface 17 | ---@field clusterer fa.ds.TileClusterer 18 | local WaterBackend = {} 19 | mod.WaterBackend = WaterBackend 20 | local WaterBackend_meta = { __index = WaterBackend } 21 | if script then script.register_metatable("fa.scanner.WaterBackend", WaterBackend_meta) end 22 | 23 | ---@param surface LuaSurface 24 | function WaterBackend.new(surface) 25 | return setmetatable( 26 | { surface = surface, clusterer = TileClusterer.TileClusterer.new({ track_interior = false }), entry_cache = {} }, 27 | WaterBackend_meta 28 | ) 29 | end 30 | 31 | ---@param e LuaEntity 32 | function WaterBackend:on_new_entity(e) end 33 | 34 | ---@param player LuaPlayer 35 | ---@param e fa.scanner.ScanEntry 36 | function WaterBackend:validate_entry(player, e) 37 | if player.surface.index ~= self.surface.index then return false end 38 | 39 | -- Could be landfilled. Check that to get our answer. 40 | return player.surface.count_tiles_filtered({ 41 | area = e.backend_data.aabb, 42 | name = SC.WATER_PROTOS, 43 | limit = 1, 44 | }) > 0 45 | end 46 | 47 | function WaterBackend:update_entry(player, e) end 48 | 49 | function WaterBackend:readout_entry(player, e) 50 | local bb = e.backend_data.aabb 51 | local w = bb.right_bottom.x - bb.left_top.x 52 | local h = bb.right_bottom.y - bb.left_top.y 53 | return { "fa.scanner-water", w, h } 54 | end 55 | 56 | ---@param player LuaPlayer 57 | ---@param callback fun(fa.scanner.ScanEntry) 58 | function WaterBackend:dump_entries_to_callback(player, callback) 59 | ---@param group fa.ds.TileClusterer.Group 60 | self.clusterer:get_groups(function(group) 61 | local tlx = math.huge 62 | local tly = math.huge 63 | local brx = -math.huge 64 | local bry = -math.huge 65 | 66 | local closest_dist = math.huge 67 | local e_x, e_y 68 | local px, py = player.position.x, player.position.y 69 | 70 | for x, children in pairs(group.edge_tiles) do 71 | tlx = tlx < x and tlx or x 72 | brx = brx > x and brx or x 73 | 74 | for y in pairs(children) do 75 | tly = tly < y and tly or y 76 | bry = bry > y and bry or y 77 | 78 | local dist = (px - x) ^ 2 + (py - y) ^ 2 79 | if dist < closest_dist then 80 | closest_dist = dist 81 | e_x = x 82 | e_y = y 83 | end 84 | end 85 | end 86 | 87 | -- This is the top-left corner of the bottom-right tile. We want the 88 | -- bottom right. 89 | brx = brx + 1 90 | bry = bry + 1 91 | 92 | callback({ 93 | -- This is fun. If we use the corner of the tile, confused geometry in 94 | -- the cursor handling code will currently corrupt the cursor to 95 | -- temporarily be off by one tile to the northwest in some and only 96 | -- some contexts. Since this won't break in future, we offset to the 97 | -- center of the tile to fix that. 98 | position = { x = e_x + 0.5, y = e_y + 0.5 }, 99 | backend_data = { 100 | aabb = { 101 | left_top = { x = tlx, y = tly }, 102 | right_bottom = { x = brx, y = bry }, 103 | }, 104 | }, 105 | backend = self, 106 | category = SC.CATEGORIES.RESOURCES, 107 | subcategory = "water", 108 | }) 109 | end) 110 | end 111 | 112 | ---@param chunk ChunkPositionAndArea 113 | function WaterBackend:on_new_chunk(chunk) 114 | local tiles = self.surface.find_tiles_filtered({ 115 | area = chunk.area, 116 | name = SC.WATER_PROTOS, 117 | }) 118 | 119 | -- Convert to xy. 120 | local xy = {} 121 | for i = 1, #tiles do 122 | table.insert(xy, tiles[i].position) 123 | end 124 | 125 | self.clusterer:submit_points(xy) 126 | end 127 | 128 | function WaterBackend:get_aabb(e) 129 | local aabb = e.backend_data.aabb 130 | local lt = aabb.left_top 131 | local rb = aabb.right_bottom 132 | return lt.x, lt.y, rb.x, rb.y 133 | end 134 | 135 | function WaterBackend:is_huge(e) 136 | return true 137 | end 138 | 139 | return mod 140 | -------------------------------------------------------------------------------- /scripts/scanner/scanner-consts.lua: -------------------------------------------------------------------------------- 1 | local mod = {} 2 | 3 | ---@enum fa.scanner.Category 4 | mod.CATEGORIES = { 5 | ALL = "all", 6 | RESOURCES = "resources", 7 | ENEMIES = "enemies", 8 | LOGISTICSAndPower = "logistics_and_power", 9 | PRODUCTION = "production", 10 | VEHICLES = "vehicles", 11 | TRAINS = "trains", 12 | GHOSTS = "ghosts", 13 | PLAYERS = "players", -- actually character. 14 | OTHER = "other", 15 | MILITARY = "military", 16 | REMNANTS = "remnants", 17 | CONTAINERS = "containers", 18 | CORPSES = "corpses", 19 | } 20 | 21 | -- The desired order of categories when moving through the scanner. 22 | mod.CATEGORY_ORDER = { 23 | mod.CATEGORIES.ALL, 24 | mod.CATEGORIES.RESOURCES, 25 | mod.CATEGORIES.ENEMIES, 26 | mod.CATEGORIES.REMNANTS, 27 | mod.CATEGORIES.PRODUCTION, 28 | mod.CATEGORIES.LOGISTICSAndPower, 29 | mod.CATEGORIES.CONTAINERS, 30 | mod.CATEGORIES.MILITARY, 31 | mod.CATEGORIES.VEHICLES, 32 | mod.CATEGORIES.TRAINS, 33 | mod.CATEGORIES.GHOSTS, 34 | mod.CATEGORIES.PLAYERS, 35 | mod.CATEGORIES.CORPSES, 36 | mod.CATEGORIES.OTHER, 37 | } 38 | 39 | -- How far can the scanner see, in tiles? 40 | -- 41 | -- Old scanner did a 5000x5000 square. This is a radius of a circle, so 2500 is 42 | -- a (rough) equivalent. 43 | mod.SCANNER_DISTANCE = 2500 44 | 45 | -- How far apart may trees be to count as a forest? Note that changing this 46 | -- value has a very outsized effect and can cause the clusterer to cluster 47 | -- thousands of wood into one forest. Also, this is effectively a radius. 48 | mod.FOREST_TREE_DIST = 4 49 | 50 | -- When this close to a forest, make an entry for the trees the player is near. 51 | mod.FOREST_ZOOM_DISTANCE = 25 52 | 53 | -- The size of chunks when handling a forest. This tunes an algorithm in the 54 | -- tree backend. 55 | -- 56 | -- IMPORTANT: changes to this value do not take effect in the current save, 57 | -- because changing it screws up the already computed information. 58 | mod.FOREST_CHUNK_SIZE = 8 59 | 60 | -- When this close to an infinite resource, instead of dumping the aggregate, 61 | -- dump the individual resources instead. 62 | mod.INFINITE_RESOURCE_ZOOM_DISTANCE = 50 63 | 64 | -- How far apart must tiles be to be in the same body of water? 2.1 is chosen 65 | -- because it allows for tiny bits of land not to get in the way, causes 66 | -- diagonal tiles to connect, and leaves a bit of room for floating point error. 67 | mod.WATER_TILE_DISTANCE = 10 68 | 69 | -- Modded water is mostly not a thing. If it is we can extend the list. 70 | mod.WATER_PROTOS = 71 | { "water", "deepwater", "water-green", "deepwater-green", "water-shallow", "water-mud", "water-wube" } 72 | 73 | return mod 74 | -------------------------------------------------------------------------------- /scripts/table-helpers.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Some common table helping algorithms. 3 | 4 | there's a number of table things like removing invalid entities which we need 5 | everywhere efficienhtly; this file does that. 6 | ]] 7 | 8 | local mod = {} 9 | 10 | --[[ 11 | Remove all items from a given array where the given callback returns true by 12 | shuffling to the end and then deleting them. The order of the returned array is 13 | unspecified. 14 | 15 | The name comes from Rust's standard library; they do an ordered variant called 16 | retain. 17 | 18 | Complexity: fast O(N) 19 | ]] 20 | ---@param a any[] 21 | ---@param filter function(any): boolean 22 | function mod.retain_unordered(a, filter) 23 | -- a is a sequence. For loops only evaluate the length once. We will be 24 | -- decreasing the length as we go. We must "hold" at a given index until we 25 | -- find an invalid entity as well. We are done when we get to the point of 26 | -- hitting a nil. The initial back is (because lua) the length of the array. 27 | -- This decreases every time an invalid entry is found. 28 | local back = #a 29 | local i = 1 30 | 31 | while true do 32 | local ent = a[i] 33 | if ent == nil then 34 | return 35 | elseif filter(ent) then 36 | -- It's good, we aren't going to be getting rid of it, move on. 37 | i = i + 1 38 | else 39 | -- If i = back and this is invalid, then it'll swap with itself and 40 | -- remain even though it shouldn't. Also, that means we're done. 41 | if i == back then 42 | a[i] = nil 43 | return 44 | end 45 | local back_ent = a[back] 46 | a[back] = nil 47 | a[i] = back_ent 48 | back = back - 1 49 | -- Don't increment i because what i is pointing at just changed and 50 | -- may, itself, be invalid. 51 | end 52 | end 53 | end 54 | 55 | -- Same as table.insert(x), except taking multiple arguments and pushing them to 56 | -- the back left to right. Behavior is incredibly undefined is nil is passed 57 | -- (LuaLS helps guard against it) 58 | -- 59 | ---@param destination any[] 60 | ---@param ... any 61 | function mod.multipush(destination, ...) 62 | local packed = table.pack(...) 63 | mod.merge_arrays(destination, packed) 64 | end 65 | 66 | -- Merges two arrays. The second array is pushed into the first. That is, it 67 | -- is *modified* in place. 68 | -- 69 | ---@param destination any[] 70 | ---@param array any[] 71 | function mod.merge_arrays(destination, array) 72 | -- faster: table.insert is a hashtable lookup. 73 | local tins = table.insert 74 | 75 | for i = 1, #array do 76 | tins(destination, array[i]) 77 | end 78 | end 79 | 80 | -- Takes an array { a, b, c } and writes to to a set { a = true, b = true, c = 81 | -- true }. 82 | ---@param set table 83 | ---@param array any[] 84 | function mod.array_to_set(set, array) 85 | for i = 1, #array do 86 | set[array[i]] = true 87 | end 88 | end 89 | 90 | -- Merge the second mapping into the first (e.g. table of non-array keys) 91 | ---@param dest table 92 | ---@param src table 93 | function mod.merge_mappings(dest, src) 94 | for k, v in pairs(src) do 95 | dest[k] = v 96 | end 97 | end 98 | 99 | local empty_table_defaulter = { 100 | __index = function(t, i) 101 | rawset(t, i, {}) 102 | return t[i] 103 | end, 104 | } 105 | if script then script.register_metatable("fa.TableHelpers.EmptyTableDefaulter", empty_table_defaulter) end 106 | 107 | --[[ 108 | Returnn an empty table. When an index not yet present in the table is accessed, 109 | fill it in with an empty table as well. If the optional argument initial is 110 | provided, wrap that instead and return it. 111 | 112 | This means code like `a[5][4]` does not need to check if 5 is present. This is 113 | very useful for making sets of 2d points and objects, but the cost is that a 114 | check like `if set[x][y]` will fill in x with an empty table even if the item is 115 | not present (but that's usually fine, because the most common operation is to 116 | then add it). 117 | 118 | IMPORTANT: the obvious extension is to allow changing ther default value. That 119 | doesn't work because global cannot hold unregistered metatables. There'd need 120 | to be a unique name each. Other methods result in losing the benefit or are 121 | much more complex and should be avoided. 122 | ]] 123 | function mod.defaulting_table(initial) 124 | local r = initial or {} 125 | assert(type(r) == "table") 126 | setmetatable(r, empty_table_defaulter) 127 | return r 128 | end 129 | 130 | --[[ 131 | Return a metatable which will, when an index is not found, iterate over all of 132 | the tables specified, left to right, before giving up. 133 | 134 | There is a particularly useful trick which allows us to provide options to 135 | functions which aren't safe for global, usually callbacks. To do it, we make 136 | the outermost table global-safe and store that. Then, we hide the 137 | non-global-safe things away in tables which are consulted by the metatable, 138 | since that never "pulls values up". This comes with a negligible performance 139 | hit, but it's usually only a couple levels and for a function, which means in 140 | context that's not too bad (plus, anything truly performance sensitive will 141 | cache in a local anyway). See e.g. ds.work_queue, scanner.backends.simple. 142 | 143 | At least one table must be specified. 144 | ]] 145 | function mod.nested_indexer(...) 146 | local args = table.pack(...) 147 | assert(#args > 0, "At least one table must be specified") 148 | local cache = {} 149 | 150 | return { 151 | __index = function(tab, key) 152 | local c = cache[key] 153 | if c then return c end 154 | 155 | for i = #args, 1, -1 do 156 | local attempt = args[i][key] 157 | if attempt then 158 | cache[key] = attempt 159 | return attempt 160 | end 161 | end 162 | 163 | return nil 164 | end, 165 | } 166 | end 167 | 168 | -- Find the index of a given element in a list. Return nil for not found. 169 | function mod.find_index_of(array, element) 170 | for i = 1, #array do 171 | if array[i] == element then return i end 172 | end 173 | 174 | -- Not found. 175 | return nil 176 | end 177 | 178 | return mod 179 | -------------------------------------------------------------------------------- /scripts/teleport.lua: -------------------------------------------------------------------------------- 1 | --Here: teleporting 2 | local fa_utils = require("scripts.fa-utils") 3 | local fa_graphics = require("scripts.graphics") 4 | local fa_mouse = require("scripts.mouse") 5 | 6 | local mod = {} 7 | 8 | --Teleports the player character to the cursor position. 9 | function mod.teleport_to_cursor(pindex, muted, ignore_enemies, return_cursor) 10 | local result = mod.teleport_to_closest(pindex, players[pindex].cursor_pos, muted, ignore_enemies) 11 | if return_cursor then players[pindex].cursor_pos = players[pindex].position end 12 | return result 13 | end 14 | 15 | --Makes the player teleport to the closest valid position to a target position. Uses game's teleport function. Muted makes silent and effectless teleporting 16 | function mod.teleport_to_closest(pindex, pos, muted, ignore_enemies) 17 | local pos = table.deepcopy(pos) 18 | local muted = muted or false 19 | local first_player = game.get_player(pindex) 20 | local surf = first_player.surface 21 | local radius = 0.5 22 | --Find a non-colliding position 23 | local new_pos = surf.find_non_colliding_position("character", pos, radius, 0.1, true) 24 | while new_pos == nil do 25 | radius = radius + 1 26 | new_pos = surf.find_non_colliding_position("character", pos, radius, 0.1, true) 27 | end 28 | --Do not teleport if in a vehicle, in a menu, or already at the desitination 29 | if first_player.vehicle ~= nil and first_player.vehicle.valid then 30 | printout("Cannot teleport while in a vehicle.", pindex) 31 | return false 32 | elseif util.distance(game.get_player(pindex).position, pos) < 0.6 then 33 | printout("Already at target", pindex) 34 | return false 35 | elseif 36 | players[pindex].in_menu 37 | and players[pindex].menu ~= "travel" 38 | and players[pindex].menu ~= "structure-travel" 39 | then 40 | printout("Cannot teleport while in a menu.", pindex) 41 | return false 42 | end 43 | --Do not teleport near enemies unless instructed to ignore them 44 | if not ignore_enemies then 45 | local enemy = 46 | first_player.surface.find_nearest_enemy({ position = new_pos, max_distance = 30, force = first_player.force }) 47 | if enemy and enemy.valid then 48 | printout( 49 | "Warning: There are enemies at this location, but you can force teleporting if you press CONTROL + SHIFT + T", 50 | pindex 51 | ) 52 | return false 53 | end 54 | end 55 | --Attempt teleport 56 | local can_port = first_player.surface.can_place_entity({ name = "character", position = new_pos }) 57 | if can_port then 58 | local old_pos = table.deepcopy(first_player.position) 59 | if not muted then 60 | --Draw teleporting visuals at origin 61 | rendering.draw_circle({ 62 | color = { 0.8, 0.2, 0.0 }, 63 | radius = 0.5, 64 | width = 15, 65 | target = old_pos, 66 | surface = first_player.surface, 67 | draw_on_ground = true, 68 | time_to_live = 60, 69 | }) 70 | rendering.draw_circle({ 71 | color = { 0.6, 0.1, 0.1 }, 72 | radius = 0.3, 73 | width = 20, 74 | target = old_pos, 75 | surface = first_player.surface, 76 | draw_on_ground = true, 77 | time_to_live = 60, 78 | }) 79 | local smoke_spot_ghosts = 80 | first_player.surface.find_entities_filtered({ position = first_player.position, type = "entity-ghost" }) 81 | if smoke_spot_ghosts == nil or #smoke_spot_ghosts == 0 then 82 | local smoke_effect = first_player.surface.create_entity({ 83 | name = "iron-chest", 84 | position = first_player.position, 85 | raise_built = false, 86 | force = first_player.force, 87 | }) 88 | smoke_effect.destroy({}) 89 | end 90 | --Teleport sound at origin 91 | game.get_player(pindex).play_sound({ path = "player-teleported", volume_modifier = 0.2, position = old_pos }) 92 | game 93 | .get_player(pindex) 94 | .play_sound({ path = "utility/scenario_message", volume_modifier = 0.8, position = old_pos }) 95 | end 96 | local teleported = false 97 | if muted then 98 | teleported = first_player.teleport(new_pos) 99 | else 100 | teleported = first_player.teleport(new_pos) 101 | end 102 | if teleported then 103 | first_player.force.chart( 104 | first_player.surface, 105 | { { new_pos.x - 15, new_pos.y - 15 }, { new_pos.x + 15, new_pos.y + 15 } } 106 | ) 107 | players[pindex].position = table.deepcopy(new_pos) 108 | reset_bump_stats(pindex) 109 | if not muted then 110 | --Draw teleporting visuals at target 111 | rendering.draw_circle({ 112 | color = { 0.3, 0.3, 0.9 }, 113 | radius = 0.5, 114 | width = 15, 115 | target = new_pos, 116 | surface = first_player.surface, 117 | draw_on_ground = true, 118 | time_to_live = 60, 119 | }) 120 | rendering.draw_circle({ 121 | color = { 0.0, 0.0, 0.9 }, 122 | radius = 0.3, 123 | width = 20, 124 | target = new_pos, 125 | surface = first_player.surface, 126 | draw_on_ground = true, 127 | time_to_live = 60, 128 | }) 129 | local smoke_spot_ghosts = 130 | first_player.surface.find_entities_filtered({ position = first_player.position, type = "entity-ghost" }) 131 | if smoke_spot_ghosts == nil or #smoke_spot_ghosts == 0 then 132 | local smoke_effect = first_player.surface.create_entity({ 133 | name = "iron-chest", 134 | position = first_player.position, 135 | raise_built = false, 136 | force = first_player.force, 137 | }) 138 | smoke_effect.destroy({}) 139 | end 140 | --Teleport sound at target 141 | game 142 | .get_player(pindex) 143 | .play_sound({ path = "player-teleported", volume_modifier = 0.2, position = new_pos }) 144 | game 145 | .get_player(pindex) 146 | .play_sound({ path = "utility/scenario_message", volume_modifier = 0.8, position = new_pos }) 147 | end 148 | if new_pos.x ~= pos.x or new_pos.y ~= pos.y then 149 | if not muted then 150 | printout( 151 | "Teleported " 152 | .. math.ceil(fa_utils.distance(pos, first_player.position)) 153 | .. " " 154 | .. fa_utils.direction(pos, first_player.position) 155 | .. " of target", 156 | pindex 157 | ) 158 | end 159 | end 160 | --Update cursor after teleport 161 | players[pindex].cursor_pos = table.deepcopy(new_pos) 162 | fa_mouse.move_mouse_pointer(fa_utils.center_of_tile(players[pindex].cursor_pos), pindex) 163 | fa_graphics.draw_cursor_highlight(pindex, nil, nil) 164 | else 165 | printout("Teleport Failed", pindex) 166 | return false 167 | end 168 | else 169 | printout("Cannot teleport", pindex) --this is unlikely to be reached because we find the first non-colliding position 170 | return false 171 | end 172 | 173 | -- --Adjust camera 174 | -- game.get_player(pindex).close_map() 175 | 176 | return true 177 | end 178 | 179 | return mod 180 | -------------------------------------------------------------------------------- /scripts/type-decls.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Shared type declarations for things we commonly need everywhere. Note that old 3 | code may not follow these. 4 | 5 | Where possible these are compatible with the *input* to the Factorio API, but 6 | the choice is more clearly made to match our current usage since we have 20k 7 | lines already. This means that Factorio might return for example a point in `{ 8 | 2, 3 }` form. In theory it does not, however, so these should generally match. 9 | ]] 10 | 11 | ---@alias fa.Point { x: number, y: number } 12 | 13 | -- If this isn't an alias LuaLS gets mad about trying to use our otherwikse 14 | -- valid boxes in the Factorio API. 15 | ---@alias fa.AABB BoundingBox.0 16 | -------------------------------------------------------------------------------- /scripts/ui/low-level/multistate-switch.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | A low-level abstraction over a toggle which deals with things like inserter read 3 | modes: "off", or a list of some values. The toggler itself is stateless, and 4 | toggling it immediately moves the entity through the various states. 5 | 6 | Note that the following API isn't exactly stable as such. This is code which is 7 | being used to partially replace less ideal code as a midpoint on the path to a 8 | proper UI system. The goal is for this to become a detail inside a declared UI 9 | hierarchy. 10 | 11 | These cannot be saved in global, and are intentionally blocked from doing so. 12 | They are static config. That restriction will be lifted in future, but we are 13 | not yet at the point of ephemeral global state. 14 | 15 | First, you declare your UI: 16 | 17 | ``` 18 | local multistate_switch = require('scripts.ui.low-level.multistate-switch') 19 | local F = require('scripts.field-ref') 20 | 21 | local switch = multistate_switch.create({ 22 | on_off_field = F.controloler.something_boolean, 23 | state_field = F.controller.inserter_read_mode_or_something, 24 | off_label = "Off", -- or "None", whatever, the "off state". 25 | choices = { 26 | -- (value, label). Will be presented in this order, which is why it's 27 | -- not a standard kv table. 28 | { defines.thing, "Reading Contents" }, 29 | { defines.other_thing, "reading something else" }, 30 | } 31 | }) 32 | ``` 33 | 34 | This gives back a table with 3 methods: `prev`, `current`, and `next`. Current 35 | has no side effects and figures out the current label; prev/next move 36 | back/forward respectively and return the new label. They all take exactly one 37 | argument: something with properties matching the two references above on it. So, 38 | for example: 39 | 40 | ``` 41 | local e = get_an_entity_somehow() 42 | local new_label = switch.next(e) 43 | -- new_label == cur_label, if and only if these are in the same tick. 44 | -- Otherwise, something else might have changed the value. 45 | local cur_label = switch.current(e) 46 | ``` 47 | 48 | Note that this abstraction does not care whether the entity is really a factorio 49 | entity. Tables work, for example. 50 | ]] 51 | local circular_list = require("scripts.ds.circular-options-list") 52 | local F = require("scripts.field-ref") -- for self-tests 53 | local methods = require("scripts.methods") 54 | 55 | local mod = {} 56 | 57 | --- @alias MultistateSwitchChoices { [1]: any, [2]: string }[] 58 | 59 | --- @class MultistateSwitchOptions 60 | --- @field on_off_field FieldRef A boolean field which will toggle it on or off. 61 | --- @field state_field FieldRef The field that contains the state value. 62 | --- @field off_label string The label to use for the off state. 63 | --- @field choices MultistateSwitchChoices The choices for the on states. 64 | 65 | local function get_cur_key(instance, from_what) 66 | return { instance.on_off_field.get(from_what), instance.state_field.get(from_what) } 67 | end 68 | 69 | local function generic_movement(instance, entity, calling, do_set) 70 | local key = get_cur_key(instance, entity) 71 | local ret = calling(instance.menu, key) 72 | assert(ret ~= nil, "Unhandled state in switch. This probably means you missed an entry in choices") 73 | key = ret.key 74 | 75 | if do_set then 76 | instance.on_off_field.set(entity, key[1]) 77 | if key[1] then 78 | -- It's on, also do the choice. 79 | instance.state_field.set(entity, key[2]) 80 | end 81 | end 82 | 83 | return ret.value.label 84 | end 85 | 86 | -- Our methods are just 3 variations on the above. 87 | --- @class MultistateSwitch 88 | local multistate_methods = {} 89 | 90 | --- @param entity table 91 | --- @returns string 92 | function multistate_methods:prev(entity) 93 | return generic_movement(self, entity, circular_list.prev, true) 94 | end 95 | 96 | --- @param entity table 97 | --- @returns string 98 | function multistate_methods:next(entity) 99 | return generic_movement(self, entity, circular_list.next, true) 100 | end 101 | 102 | --- @param entity table 103 | --- @returns string 104 | function multistate_methods:current(entity) 105 | return generic_movement(self, entity, circular_list.current, false) 106 | end 107 | 108 | local linker = methods.link("multistate-switch", multistate_methods) 109 | 110 | --- @param opts MultistateSwitchOptions 111 | --- @returns MultistateSwitch 112 | function mod.create(opts) 113 | -- What we are actually going to do is compile to a circular list and save 114 | -- that. 115 | local instance = { 116 | on_off_field = opts.on_off_field, 117 | state_field = opts.state_field, 118 | __no_global = function() end, 119 | } 120 | 121 | -- Our keys are { onoff, state } and values { label = message }, using the 122 | -- wildcard to capture all the off states into one entry. 123 | 124 | choices = { 125 | { { false, circular_list.ANY }, { label = opts.off_label } }, 126 | } 127 | 128 | for i = 1, #opts.choices do 129 | table.insert(choices, { { true, opts.choices[i][1] }, { label = opts.choices[i][2] } }) 130 | end 131 | local choice_list = circular_list.kv_list(choices, circular_list.tuples) 132 | 133 | instance.menu = choice_list 134 | linker(instance) 135 | return instance 136 | end 137 | 138 | -- here come the self tests. 139 | local fake_entity = { on = false, state = 0 } 140 | local test_switch = mod.create({ 141 | on_off_field = F.on(), 142 | state_field = F.state(), 143 | off_label = "is off", 144 | choices = { 145 | { 0, "is 0" }, 146 | { 1, "is 1" }, 147 | { 2, "is 2" }, 148 | }, 149 | }) 150 | 151 | assert(test_switch.current(fake_entity) == "is off") 152 | 153 | assert(test_switch.next(fake_entity) == "is 0") 154 | assert(fake_entity.on == true) 155 | assert(fake_entity.state == 0) 156 | 157 | assert(test_switch.next(fake_entity) == "is 1") 158 | assert(fake_entity.on == true) 159 | assert(fake_entity.state == 1) 160 | 161 | assert(test_switch.next(fake_entity) == "is 2") 162 | assert(fake_entity.on == true) 163 | assert(fake_entity.state == 2) 164 | 165 | assert(test_switch.next(fake_entity) == "is off") 166 | assert(fake_entity.on == false) 167 | -- When switching to off, the other property is left alone. 168 | assert(fake_entity.state == 2) 169 | 170 | assert(test_switch.prev(fake_entity) == "is 2") 171 | assert(test_switch.prev(fake_entity) == "is 1") 172 | assert(fake_entity.on == true) 173 | assert(fake_entity.state == 1) 174 | 175 | return mod 176 | -------------------------------------------------------------------------------- /scripts/uid.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Unique ids 3 | 4 | Sometimes it is very useful to generate ids which are unique per save. This is 5 | used for example in rulers. 6 | 7 | At first it may seem that one might simply use tables. This works for 8 | everything but deletion. Usually a table is one level too deep for removal. By 9 | using an integral id, removal itself becomes easy. See rulers.lua for the 10 | pattern; that was left with extra comments for the sake of being able to see the 11 | why of it. 12 | 13 | ]] 14 | 15 | local mod = {} 16 | 17 | ---@returns number 18 | function mod.uid() 19 | if not global.id_counter then global.id_counter = 0 end 20 | global.id_counter = global.id_counter + 1 21 | return global.id_counter 22 | end 23 | 24 | return mod 25 | -------------------------------------------------------------------------------- /scripts/work-queue.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | A work queue takes some number of work items and runs them across some number of 3 | ticks. This is used to amortize the cost of long-running operations across 4 | ticks so that UPS stays reasonable. 5 | 6 | To use it, call declare_work_queue to make a work queue. This will wire up a 7 | queue which will call a specified function and use a specified key in global for 8 | the state. Queues cannot currently be shared or stored in global by the 9 | user--they're singletons. This may later be fixed with a migration which kills 10 | the work_queues key of global, then starts returning a full state. That's not 11 | hard, as one can use a similar trick to scripts.ds.clusterer, just not worth it 12 | as of this writing (2024-09-05). Note that such a replacement must also solve 13 | being registered with a central registry, which is a non-obvious problem. 14 | Otherwise they can't advance with this module's on_tick. 15 | 16 | Each enqueued item is of whatever form the caller wants and consequently can 17 | carry state or etc. For more complex patterns the last item enqueued by the 18 | caller will be called last and can be used to update structures external to the 19 | queue model. These items are stored in global and cannot contain functions or 20 | closures directly. Instead, use a key that indexes into a table of your own to 21 | get the function when the item runs. 22 | 23 | It is safe to modify (including clearing) the work queue while inside an item's 24 | callback. 25 | 26 | For modules which wish to respawn work (e.g. scanner), a function 27 | `idle_function` may be specified. This gets called if there is a tick where the 28 | queue is empty. 29 | ]] 30 | local Deque = require("scripts.ds.deque") 31 | 32 | local mod = {} 33 | 34 | ---@class fa.WorkQueueHandle 35 | ---@field name string 36 | ---@field worker_function fun(item: any) 37 | ---@field idle_function (fun(fa.WorkQueueHandle))? 38 | ---@field per_tick number 39 | local WorkQueueHandle = {} 40 | local work_queue_handle_meta = { __index = WorkQueueHandle } 41 | if script then script.register_metatable("fa.WorkQueue", work_queue_handle_meta) end 42 | 43 | -- This state, held outside global, is reconstituted on imports. It is used to 44 | -- let this module know about all work queues. 45 | ---@type fa.WorkQueueHandle[] 46 | local queues = {} 47 | 48 | -- This set ensures that no queue is declared more than twice. 49 | ---@type table 50 | local declared_names = {} 51 | 52 | ---@class fa.WorkQueueOpts 53 | ---@field name string Must be globally unique. 54 | ---@field worker_function fun(any) 55 | ---@field idle_function (fun(fa.WorkQueueHandle))? 56 | ---@field per_tick number how many items to dequeue per tick 57 | 58 | ---@param opts fa.WorkQueueOpts 59 | ---@returns fa.WorkQueueHandle 60 | function mod.declare_work_queue(opts) 61 | assert(not declared_names[opts.name], "Attempt to declare queues of the same name: name=" .. opts.name) 62 | declared_names[opts.name] = true 63 | 64 | local qstate = { 65 | name = opts.name, 66 | worker_function = opts.worker_function, 67 | idle_function = opts.idle_function, 68 | per_tick = opts.per_tick, 69 | } 70 | setmetatable(qstate, work_queue_handle_meta) 71 | table.insert(queues, qstate) 72 | return qstate 73 | end 74 | 75 | -- For dev. Set to true to make work queues reset themselves on game restarts. 76 | local force_clear = false 77 | 78 | ---@returns { items: fa.ds.Deque } 79 | function qstate_from_global(name) 80 | if force_clear then 81 | global.work_queues = {} 82 | force_clear = false 83 | end 84 | global.work_queues = global.work_queues or {} 85 | if not global.work_queues[name] then global.work_queues[name] = { 86 | items = Deque.Deque.new(), 87 | } end 88 | 89 | return global.work_queues[name] 90 | end 91 | 92 | function WorkQueueHandle:enqueue(item) 93 | local state = qstate_from_global(self.name) 94 | state.items:push_back(item) 95 | end 96 | 97 | function WorkQueueHandle:clear() 98 | local state = qstate_from_global(self.name) 99 | state.items:clear() 100 | end 101 | 102 | function mod.on_tick() 103 | for _, q in pairs(queues) do 104 | local state = qstate_from_global(q.name) 105 | local did = 0 106 | for i = 1, q.per_tick do 107 | local item = state.items:pop_front() 108 | if not item then break end 109 | did = did + 1 110 | q.worker_function(item) 111 | end 112 | 113 | if did == 0 and q.idle_function then q.idle_function(q) end 114 | end 115 | end 116 | 117 | return mod 118 | -------------------------------------------------------------------------------- /scripts/zoom.lua: -------------------------------------------------------------------------------- 1 | --Here: Functions about the zoom system 2 | local fa_graphics = require("scripts.graphics") 3 | 4 | local ZOOM_PER_TICK = 1.104086977 5 | local ln_zoom = math.log(ZOOM_PER_TICK) 6 | 7 | local mod = {} 8 | 9 | mod.MIN_ZOOM = 0.275 10 | mod.MAX_ZOOM = 3.282 11 | 12 | function mod.get_zoom_tick(pindex) 13 | return math.floor(math.log(global.players[pindex].zoom) / ln_zoom + 0.5) 14 | end 15 | 16 | function mod.tick_to_zoom(zoom_tick) 17 | return ZOOM_PER_TICK ^ zoom_tick 18 | end 19 | 20 | function mod.fix_zoom(pindex) 21 | game.players[pindex].zoom = global.players[pindex].zoom 22 | end 23 | 24 | function mod.set_zoom(value, pindex) 25 | --Note zoom levels: 26 | game.players[pindex].zoom = value 27 | global.players[pindex].zoom = value 28 | end 29 | 30 | local function zoom_change(pindex, etick, change_by_tick) 31 | -- if global.players[pindex].last_zoom_event_tick == etick then 32 | -- print("maybe duplicate") 33 | -- return 34 | -- end 35 | -- global.players[pindex].last_zoom_event_tick = etick 36 | if game.players[pindex].render_mode == defines.render_mode.game then 37 | local tick = mod.get_zoom_tick(pindex) 38 | tick = tick + change_by_tick 39 | local zoom = mod.tick_to_zoom(tick) 40 | if zoom < mod.MAX_ZOOM and zoom > mod.MIN_ZOOM then 41 | global.players[pindex].zoom = zoom 42 | local stack = game.get_player(pindex).cursor_stack 43 | if stack and stack.valid_for_read and stack.valid and stack.prototype.place_result ~= nil then 44 | fa_graphics.sync_build_cursor_graphics(pindex) 45 | else 46 | fa_graphics.draw_cursor_highlight(pindex, nil, nil) 47 | end 48 | end 49 | end 50 | end 51 | 52 | function mod.zoom_in(event) 53 | zoom_change(event.player_index, event.tick, 1) 54 | end 55 | 56 | function mod.zoom_out(event) 57 | zoom_change(event.player_index, event.tick, -1) 58 | end 59 | 60 | script.on_event("fa-zoom-in", mod.zoom_in) 61 | script.on_event("fa-zoom-out", mod.zoom_out) 62 | script.on_event(defines.events.on_cutscene_waypoint_reached, function(event) 63 | if game.players[event.player_index].render_mode == defines.render_mode.game then mod.fix_zoom(event.player_index) end 64 | end) 65 | script.on_event("fa-debug-reset-zoom", function(event) 66 | global.players[event.player_index].zoom = 1 67 | end) 68 | script.on_event("fa-debug-reset-zoom-2x", function(event) 69 | global.players[event.player_index].zoom = 2 70 | end) 71 | 72 | return mod 73 | -------------------------------------------------------------------------------- /settings-updates.lua: -------------------------------------------------------------------------------- 1 | data:extend({ 2 | { 3 | type = "int-setting", 4 | name = "VehicleSnap_amount", 5 | setting_type = "runtime-per-user", 6 | minimum_value = 4, 7 | default_value = 8, 8 | }, 9 | { 10 | type = "string-setting", 11 | name = "aai-loaders-mode", 12 | setting_type = "startup", 13 | default_value = "expensive", 14 | allowed_values = { "lubricated", "expensive", "graphics-only" }, 15 | order = "a", 16 | }, 17 | { 18 | type = "bool-setting", 19 | name = "PDA-setting-smart-roads-enabled", 20 | setting_type = "startup", 21 | default_value = true, 22 | order = "ab", 23 | }, 24 | { 25 | type = "int-setting", 26 | name = "PDA-setting-assist-min-speed", 27 | setting_type = "runtime-global", 28 | default_value = 9, 29 | minimum_value = 6, 30 | maximum_value = 10000, 31 | order = "d", 32 | }, 33 | }) 34 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | indent_type = "Spaces" 2 | indent_width = 3 3 | collapse_simple_statement = "ConditionalOnly" 4 | -------------------------------------------------------------------------------- /thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Factorio-Access/FactorioAccess/a310812b7a06877148c55b19d7b05eb427b44eca/thumbnail.png --------------------------------------------------------------------------------