├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md └── workflows │ ├── autoPublish.yml │ └── lint.yml ├── .lua-format ├── .scripts ├── handle_sounds.sh ├── upload.sh └── zip_mod.sh ├── README.md ├── changelog.txt ├── const-commands.lua ├── control.lua ├── data-final-fixes.lua ├── data-updates.lua ├── data.lua ├── defines.lua ├── info.json ├── locale └── en │ └── example-mod.cfg ├── migrations └── example-mod_0.2.6.lua ├── models ├── data-consistency-example.lua ├── empty-module.lua └── example-module.lua ├── scenarios └── example │ ├── control.lua │ ├── image.png │ ├── info.json │ ├── locale │ └── en │ │ └── level.cfg │ └── models │ ├── example-module.lua │ └── stop-example-mod.lua ├── settings-final-fixes.lua ├── settings-updates.lua ├── settings.lua ├── special-info.md ├── switchable-commands.lua └── thumbnail.png /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | 12 | [*.{lua,lua2p,sh,cmd,can}] 13 | indent_style = tab 14 | insert_final_newline = true 15 | 16 | [*.{txt,yml,json}] 17 | indent_style = space 18 | indent_size = 2 19 | insert_final_newline = false 20 | 21 | [*.cfg] 22 | indent_style = space 23 | indent_size = 2 24 | insert_final_newline = true 25 | trim_trailing_whitespace = false 26 | 27 | [*.md] 28 | indent_style = tab 29 | indent_size = 2 30 | insert_final_newline = false 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: ZwerOxotnik 7 | 8 | --- 9 | 10 | Please drop the `factorio-current.log` file or `factorio-previous.log` here 11 | 12 | **Describe the bug** 13 | 14 | 15 | **Steps to reproduce** 16 | 1. 17 | 2. 18 | 3. 19 | 20 | **Expected behavior** 21 | 22 | 23 | **Screenshots/Videos (If you have any)** 24 | 25 | 26 | 27 | **Additional Information** 28 | Add any other context about the problem here? 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE REQUEST]" 5 | labels: feature request 6 | assignees: ZwerOxotnik 7 | 8 | --- 9 | 10 | **Is your feature request related to a issue? Please describe.** 11 | 12 | 13 | **Describe the suggestion (Use a lot of details if it is complex)** 14 | 15 | 16 | **Screenshots/Videos (If you have any that will help us)** 17 | 18 | 19 | **Additional Information** 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: You have a question that you would like answered 4 | title: "[QUESTION]" 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Please put your question below** 11 | -------------------------------------------------------------------------------- /.github/workflows/autoPublish.yml: -------------------------------------------------------------------------------- 1 | name: Auto publish 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'info.json' # Triggers only if the mod info file is updated 7 | branches: 8 | - main 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository and submodules 15 | uses: actions/checkout@v3 16 | with: 17 | submodules: recursive 18 | - name: get mod_version 19 | id: mod_version 20 | uses: notiz-dev/github-action-json-property@release 21 | with: 22 | path: 'info.json' 23 | prop_path: 'version' 24 | - name: get factorio_version 25 | id: factorio_version 26 | uses: notiz-dev/github-action-json-property@release 27 | with: 28 | path: 'info.json' 29 | prop_path: 'factorio_version' 30 | - name: get mod name 31 | id: mod_name 32 | uses: notiz-dev/github-action-json-property@release 33 | with: 34 | path: 'info.json' 35 | prop_path: 'name' 36 | - name: Zip mod 37 | run: bash ./.scripts/zip_mod.sh 38 | - name: Upload the mod on mods.factorio.com 39 | env: 40 | FACTORIO_MOD_API_KEY: ${{ secrets.FACTORIO_MOD_API_KEY }} 41 | run: bash ./.scripts/upload.sh 42 | - uses: marvinpinto/action-automatic-releases@latest 43 | id: aar 44 | with: 45 | automatic_release_tag: "${{steps.mod_version.outputs.prop}}" 46 | title: "For factorio ${{steps.factorio_version.outputs.prop}}" 47 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 48 | prerelease: false 49 | files: | 50 | ./${{steps.mod_name.outputs.prop}}_${{steps.version.outputs.prop}}.zip -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | name: Lint 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: lint 13 | uses: Roang-zero1/factorio-mod-luacheck@master 14 | with: 15 | luacheckrc_url: https://github.com/Nexela/Factorio-luacheckrc/raw/master/.luacheckrc 16 | -------------------------------------------------------------------------------- /.lua-format: -------------------------------------------------------------------------------- 1 | column_limit: &1 120 2 | indent_width: 1 3 | use_tab: true 4 | tab_width: 4 5 | continuation_indent_width: 4 6 | spaces_before_call: 0 7 | keep_simple_control_block_one_line: false 8 | keep_simple_function_one_line: false 9 | align_args: false 10 | break_after_functioncall_lp: false 11 | break_before_functioncall_rp: false 12 | align_parameter: false 13 | chop_down_parameter: false 14 | break_after_functiondef_lp: false 15 | break_before_functiondef_rp: true 16 | align_table_field: false 17 | break_after_table_lb: true 18 | break_before_table_rb: true 19 | chop_down_table: false 20 | chop_down_kv_table: true 21 | table_sep: "," 22 | extra_sep_at_table_end: false 23 | column_table_limit: *1 24 | break_after_operator: true 25 | double_quote_to_single_quote: false 26 | single_quote_to_double_quote: true 27 | -------------------------------------------------------------------------------- /.scripts/handle_sounds.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | (set -o igncr) 2>/dev/null && set -o igncr; # This comment is required. 3 | ### The above line ensures that the script can be run on Cygwin/Linux even with Windows CRNL. 4 | ### Generates lua and cfg files to handle .ogg sounds for Factorio 5 | ### Modified version of https://github.com/ZwerOxotnik/Mod-generator 6 | 7 | 8 | main() { 9 | local bold=$(tput bold) 10 | local normal=$(tput sgr0) 11 | 12 | 13 | ### Find info.json 14 | local SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 15 | cd $SCRIPT_DIR 16 | local infojson_exists=false 17 | local script_file=`basename "$0"` 18 | if [[ -s "$SCRIPT_DIR/info.json" ]]; then 19 | local infojson_exists=true 20 | else 21 | cd .. 22 | if [[ -s "$PWD/info.json" ]]; then 23 | local infojson_exists=true 24 | else 25 | cd $SCRIPT_DIR 26 | fi 27 | fi 28 | local mod_folder=$PWD 29 | 30 | 31 | ### Check if sox command exists 32 | ### https://sox.sourceforge.net/ 33 | local sox_exists=false 34 | if command -v ls &> /dev/null; then 35 | sox_exists=true 36 | fi 37 | 38 | 39 | local SOUNDS_LIST_FILE=sounds_list.lua 40 | local CFG_FILE=sounds_list.cfg 41 | 42 | 43 | ### Get mod name and version from info.json 44 | ### https://stedolan.github.io/jq/ 45 | if [ $infojson_exists = true ] ; then 46 | local MOD_NAME=$(jq -r '.name' info.json) 47 | if ! command -v jq &> /dev/null; then 48 | echo "Please install jq https://stedolan.github.io/jq/" 49 | fi 50 | fi 51 | 52 | 53 | echo "you're in ${bold}$mod_folder${normal}" 54 | 55 | read -r -p "Complete path to folder of sounds: $MOD_NAME/" folder_name 56 | local folder_path=$mod_folder/$folder_name 57 | if [ ! -z "$folder_name" ]; then 58 | local rel_folder_path="${folder_name}/" 59 | fi 60 | 61 | read -r -p "Add sounds to programmable speakers? [Y/N] " response 62 | case "$response" in 63 | [yY][eE][sS]|[yY]) 64 | STATE=1 65 | ;; 66 | *) 67 | STATE=2 68 | ;; 69 | esac 70 | if [ $STATE -eq 1 ]; then 71 | read -r -p "Insert group name of sounds:" sound_group_name 72 | case "$sound_group_name" in "") 73 | local sound_group_name=$MOD_NAME 74 | ;; 75 | esac 76 | fi 77 | 78 | 79 | local SOUNDS_LIST_PATH="$folder_path/$SOUNDS_LIST_FILE" 80 | rm -f $SOUNDS_LIST_PATH 81 | local CFG_FILE="generated_$sound_group_name".cfg 82 | if [ $infojson_exists = true ] ; then 83 | local CFG_FULLPATH=$mod_folder/locale/en/$CFG_FILE 84 | mkdir -p $mod_folder/locale/en 85 | else 86 | local CFG_FULLPATH=$mod_folder/$CFG_FILE 87 | fi 88 | rm -f $CFG_FULLPATH 89 | 90 | if [ $STATE -eq 1 ]; then 91 | echo "### This file auto-generated by https://github.com/ZwerOxotnik/factorio-example-mod" >> $CFG_FULLPATH 92 | echo "### Please, do not change this file manually!" >> $CFG_FULLPATH 93 | echo "[programmable-speaker-instrument]" >> $CFG_FULLPATH 94 | echo $sound_group_name=$sound_group_name >> $CFG_FULLPATH 95 | echo "[programmable-speaker-note]" >> $CFG_FULLPATH 96 | fi 97 | echo "-- This file auto-generated by https://github.com/ZwerOxotnik/factorio-example-mod" >> $SOUNDS_LIST_PATH 98 | echo "-- Please, do not change this file if you're not sure, except sounds_list.name and path!" >> $SOUNDS_LIST_PATH 99 | echo "-- You need require this file to your control.lua and add https://mods.factorio.com/mod/zk-lib in your dependencies" >> $SOUNDS_LIST_PATH 100 | echo "" >> $SOUNDS_LIST_PATH 101 | echo "local sounds_list = {" >> $SOUNDS_LIST_PATH 102 | 103 | if [ $STATE -eq 1 ]; then 104 | echo -e "\tname = \"$sound_group_name\", --change me, if you want to add these sounds to programmable speakers" >> $SOUNDS_LIST_PATH 105 | fi 106 | if [ $STATE -eq 2 ]; then 107 | echo -e "\tname = nil --change me, if you want to add these sounds to programmable speakers" >> $SOUNDS_LIST_PATH 108 | fi 109 | echo -e "\tpath = \"__"${MOD_NAME}"__/"${rel_folder_path}"\", -- path to this folder" >> $SOUNDS_LIST_PATH 110 | echo -e "\tsounds = {" >> $SOUNDS_LIST_PATH 111 | 112 | 113 | ###Converts audio files to .ogg format 114 | if [ $sox_exists = true ] ; then 115 | local files=($(find $folder_path/ -type f)) 116 | for fullpath in "${files[@]}"; do 117 | ### Took from https://stackoverflow.com/a/1403489 118 | local filename="${fullpath##*/}" # Strip longest match of */ from start 119 | local dir="${fullpath:0:${#fullpath} - ${#filename}}" # Substring from 0 thru pos of filename 120 | local base="${filename%.[^.]*}" # Strip shortest match of . plus at least one non-dot char from end 121 | local ext="${filename:${#base} + 1}" # Substring from len of base thru end 122 | if [[ -z "$base" && -n "$ext" ]]; then # If we have an extension and no base, it's really the base 123 | local base=".$ext" 124 | local ext="" 125 | fi 126 | ### It's too messy to fix 127 | if ! [[ "$ext" =~ ^(|ogg|txt|lua|zip|json|cfg|md|sample|bat|sh|gitignore|pack|idx|yml|png)$ ]]; then 128 | sox $fullpath $dir/$base.ogg 129 | fi 130 | done 131 | fi 132 | 133 | local format=*.ogg 134 | local files=($(find $folder_path/ -name "$format" -type f)) 135 | for path in "${files[@]}"; do 136 | local name="$(basename -- $path)" 137 | local name=${name%.*} 138 | echo -e "\t\t{" >> $SOUNDS_LIST_PATH 139 | echo -e "\t\t\tname = \"$name\"", >> $SOUNDS_LIST_PATH 140 | echo -e "\t\t}," >> $SOUNDS_LIST_PATH 141 | if [ $STATE -eq 1 ]; then 142 | echo $name=$name >> $CFG_FULLPATH 143 | fi 144 | done 145 | echo -e "\t}" >> $SOUNDS_LIST_PATH 146 | echo "}" >> $SOUNDS_LIST_PATH 147 | echo "" >> $SOUNDS_LIST_PATH 148 | echo "if puan2_api then" >> $SOUNDS_LIST_PATH 149 | echo " puan2_api.add_sounds(sounds_list)" >> $SOUNDS_LIST_PATH 150 | echo "elseif puan_api then" >> $SOUNDS_LIST_PATH 151 | echo " puan_api.add_sounds(sounds_list)" >> $SOUNDS_LIST_PATH 152 | echo "end" >> $SOUNDS_LIST_PATH 153 | echo "" >> $SOUNDS_LIST_PATH 154 | echo "return sounds_list" >> $SOUNDS_LIST_PATH 155 | 156 | echo "" 157 | echo "You're almost ready!${bold}" 158 | 159 | if [ ! -s "$mod_folder/control.lua" ] && [ $infojson_exists = true ]; then 160 | echo "require(\"__${MOD_NAME}__/${rel_folder_path}sounds_list\")" >> "$mod_folder/control.lua" 161 | else 162 | if [ $infojson_exists = true ]; then 163 | echo "# You need to write 'require(\"__${MOD_NAME}__/${rel_folder_path}sounds_list\")' in your ${MOD_NAME}/control.lua" 164 | else 165 | echo "# You need to write 'require(\"__mod-name__/${rel_folder_path}sounds_list\")' in your ${MOD_NAME}/control.lua" 166 | fi 167 | fi 168 | echo "# Add string \"zk-lib\" in dependencies of ${MOD_NAME}/info.json, example: '\"dependencies\": [\"zk-lib\"]'" 169 | if [ $STATE -eq 1 ] && [ $infojson_exists = false ]; then 170 | echo "# Put ${CFG_FILE} in folder /locale/en (it'll provide readable text in the game)" 171 | fi 172 | 173 | echo "${normal}" 174 | echo "" 175 | 176 | echo "if you found a bug or you have a problem with script etc, please, let me know" 177 | echo "This script created by ZwerOxotnik (source: https://github.com/ZwerOxotnik/Mod-generator)" 178 | } 179 | main 180 | -------------------------------------------------------------------------------- /.scripts/upload.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | (set -o igncr) 2>/dev/null && set -o igncr; # This comment is required. 3 | ### The above line ensures that the script can be run on Cygwin/Linux even with Windows CRNL. 4 | ### Run this script on GitHub after zipping the mod to upload the mod on mods.factorio.com 5 | ### This a modified version of https://github.com/Penguin-Spy/factorio-mod-portal-publish/blob/5c669dc60672e6293afd5b1913a0c8475c5490c0/entrypoint.sh 6 | ### You can generate your FACTORIO_MOD_API_KEY on https://factorio.com/create-api-key 7 | 8 | 9 | main() { 10 | ### Check commands 11 | local has_errors=false 12 | if ! command -v jq &> /dev/null; then 13 | echo "Please install jq https://stedolan.github.io/jq/" 14 | local has_errors=true 15 | fi 16 | if [ $has_errors = true ] ; then 17 | exit 1 18 | fi 19 | 20 | 21 | ### Get mod name and version from info.json 22 | ### https://stedolan.github.io/jq/ 23 | local MOD_VERSION=$(jq -r '.version' info.json) 24 | local MOD_NAME=$(jq -r '.name' info.json) 25 | 26 | 27 | # Validate the version string we're building 28 | if ! echo "${MOD_VERSION}" | grep -P --quiet '^\d+\.\d+\.\d+$'; then 29 | echo "Incorrect version pattern, needs to be %u.%u.%u (e.q., 0.1.0)" 30 | exit 1 31 | fi 32 | 33 | 34 | # Get an upload url for the mod 35 | local URL_RESULT=$(curl -s -d "mod=${MOD_NAME}" -H "Authorization: Bearer ${FACTORIO_MOD_API_KEY}" https://mods.factorio.com/api/v2/mods/releases/init_upload) 36 | local UPLOAD_URL=$(echo "${URL_RESULT}" | jq -r '.upload_url') 37 | if [[ "${UPLOAD_URL}" == "null" ]] || [[ -z "${UPLOAD_URL}" ]]; then 38 | echo "Couldn't get an upload url, failed" 39 | local ERROR=$(echo "${URL_RESULT}" | jq -r '.error') 40 | local MESSAGE=$(echo "${URL_RESULT}" | jq -r '.message // empty') 41 | echo "${ERROR}: ${MESSAGE}" 42 | exit 1 43 | fi 44 | 45 | 46 | # Upload the file 47 | local UPLOAD_RESULT=$(curl -s -F "file=@${MOD_NAME}_${MOD_VERSION}.zip" "${UPLOAD_URL}") 48 | 49 | # The success attribute only appears on successful uploads 50 | local SUCCESS=$(echo "${UPLOAD_RESULT}" | jq -r '.success') 51 | if [[ "${SUCCESS}" == "null" ]] || [[ -z "${SUCCESS}" ]]; then 52 | echo "Upload failed" 53 | local ERROR=$(echo "${UPLOAD_RESULT}" | jq -r '.error') 54 | local MESSAGE=$(echo "${UPLOAD_RESULT}" | jq -r '.message // empty') 55 | echo "${ERROR}: ${MESSAGE}" 56 | exit 1 57 | fi 58 | 59 | echo "Upload of ${MOD_NAME}_${MOD_VERSION}.zip completed" 60 | } 61 | main 62 | -------------------------------------------------------------------------------- /.scripts/zip_mod.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | (set -o igncr) 2>/dev/null && set -o igncr; # This comment is required. 3 | ### The above line ensures that the script can be run on Cygwin/Linux even with Windows CRNL. 4 | ### Run this script after updating the mod to prepare a zip of it. 5 | 6 | main() { 7 | ### Check commands 8 | if ! command -v git &> /dev/null; then 9 | echo "Please install/use git https://git-scm.com/downloads" 10 | fi 11 | local has_errors=false 12 | if ! command -v jq &> /dev/null; then 13 | echo "Please install jq https://stedolan.github.io/jq/" 14 | local has_errors=true 15 | fi 16 | if ! command -v 7z &> /dev/null; then 17 | echo "Please install 7-Zip https://www.7-zip.org/download.html" 18 | local has_errors=true 19 | fi 20 | if [ $has_errors = true ] ; then 21 | exit 1 22 | fi 23 | 24 | 25 | ### mod_folder is a mod directory with info.json 26 | local init_dir=`pwd` 27 | 28 | 29 | ### Find info.json 30 | local SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 31 | local infojson_exists=false 32 | if [[ -s "$SCRIPT_DIR/info.json" ]]; then 33 | infojson_exists=true 34 | mod_folder=$SCRIPT_DIR 35 | else 36 | cd $SCRIPT_DIR 37 | cd .. 38 | if [[ -s "$PWD/info.json" ]]; then 39 | infojson_exists=true 40 | mod_folder=$PWD 41 | elif [[ -s "$init_dir/info.json" ]]; then 42 | infojson_exists=true 43 | mod_folder=$init_dir 44 | fi 45 | fi 46 | 47 | 48 | if [ $infojson_exists = false ] ; then 49 | echo "There's no info.json" 50 | exit 1 51 | fi 52 | cd "$mod_folder/" 53 | echo "Target folder: ${mod_folder}" 54 | 55 | 56 | ### Get mod name and version from info.json 57 | ### https://stedolan.github.io/jq/ 58 | local MOD_NAME=$(jq -r '.name' info.json) 59 | local MOD_VERSION=$(jq -r '.version' info.json) 60 | 61 | 62 | # Validate the version string we're building 63 | if ! echo "${MOD_VERSION}" | grep -P --quiet '^\d+\.\d+\.\d+$'; then 64 | echo "Incorrect version pattern, needs to be %u.%u.%u (e.q., 0.1.0)" 65 | exit 1 66 | fi 67 | 68 | 69 | ### Prepare zip for Factorio native use and mod portal 70 | ### https://www.7-zip.org/download.html 71 | local name="${MOD_NAME}_${MOD_VERSION}" 72 | if command -v git &> /dev/null; then 73 | git clean -xdf 74 | fi 75 | 7z a -xr'!.*' "${mod_folder}/${name}.zip" "${mod_folder}" 76 | } 77 | main 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **[Notes](#notes)** | 2 | **[Contributing](#contributing)** | 3 | **[License](#license)** 4 | 5 | --- 6 | 7 |

8 | Example mod 13 |

14 | 15 |

16 | 17 | Release 18 | 19 | 20 | Star 21 | 22 | 23 | Discord 24 |
25 |
26 | Patreon 27 | 28 | Buy me a coffee 29 | 30 | Fork 31 | 32 |

33 | 34 |

35 | Changelog 36 | · 37 | Translations 38 |

39 | 40 |

41 | 42 | 43 | 47 | 48 | Example mod 49 | ----------------------- 50 | 51 | Lightweight modular example mod with various features and compatibilities 52 | 53 |

54 | Download the mod  ▶ 55 |

56 | 57 | What it can do 58 | -------------- 59 | 60 | * Filter parameters of commands 61 | * Add switchable, customizable commands via map settings 62 | * Double check of commands 63 | * Use built-in error handling of commands 64 | * Use modular structure 65 | * Remotely and safely disable your mod 66 | * Auto adds remote access for rcon and for other mods/scenarios 67 | * Auto publishing on [mods.portal.com](https://mods.factorio.com/) and on your GitHub repository 68 | 69 | What it enables you to do 70 | ------------------------- 71 | 72 | * Handle sounds by a [script](.scripts/handle_sounds.sh) 73 | * Make switchable, simpler and safer [commands](https://github.com/ZwerOxotnik/factorio-BetterCommands) 74 | * Make "isolated" modules 75 | * Expand your modules 76 | * More possibilities to control logic 77 | * Quickly publish your mod on mod portal 78 | * Use other's modules/code without adaptation 79 | * Auto publishing everywhere (almost) 80 | * Easy maintaining 81 | 82 | Useful stuff 83 | ------------ 84 | 85 | * [Lua 5.2 Reference Manual](https://www.lua.org/manual/5.2/), [Introduction to Lua by Dibyendu Majumdar](https://the-ravi-programming-language.readthedocs.io/en/latest/lua-introduction.html) 86 | * Factorio modding: https://wiki.factorio.com/Modding 87 | * GitHub service for localization via crowdin: https://github.com/dima74/factorio-mods-localization 88 | * Optimisation tips: https://stigmax.gitbook.io/lua-guide/auxiliary/optimizations & http://lua-users.org/wiki/OptimisationTips & https://springrts.com/wiki/Lua_Performance & https://www.lua.org/gems/sample.pdf & [tips](https://stackoverflow.com/questions/154672/what-can-i-do-to-increase-the-performance-of-a-lua-program) & [tips](https://www.programmersought.com/article/62883257108/) (please, notice that Factorio uses modified version of Lua [5.2.1](https://lua-api.factorio.com/latest/Libraries.html), so some tips might be **irrelevant** etc. for Factorio mods!) & [Performance Comparison](https://eklausmeier.wordpress.com/2020/05/14/performance-comparison-pallene-vs-lua-5-1-5-2-5-3-5-4-vs-c/) & https://gitspartv.github.io/LuaJIT-Benchmarks/ 89 | * EmmyLua Annotations: [lua-language-server/wiki/EmmyLua-Annotations][EmmyLua-Annotations] 90 | * Untitled GUI Guide: https://github.com/ClaudeMetz/UntitledGuiGuide/wiki (comprehensive tutorial on building custom interfaces) 91 | * Code Completion for jetbrains IDEs: https://forums.factorio.com/viewtopic.php?f=135&p=567132 92 | 93 | Stuff used 94 | ---------- 95 | 96 | * Many GitHub Actions to automatically publish to the Factorio mod portal etc 97 | * [EditorConfig](https://editorconfig.org/) - helps maintain consistent coding styles for multiple developers working on the same project across various editors and IDEs 98 | * Factorio's event handler (See Factorio's folder `..\data\core\lualib\event_handler.lua`) 99 | * [git] - version control system 100 | * [Visual Studio Code](https://code.visualstudio.com/) - code editor 101 | * [Lua Language Server](https://github.com/sumneko/lua-language-server) ([Setting without VSCode](https://github.com/sumneko/lua-language-server/wiki/Setting-without-VSCode)) 102 | * [EmmyLua Annotation][EmmyLua-Annotations] 103 | * Some ZwerOxotnik's code 104 | * [factorio-mod-luacheck][factorio-mod-luacheck] - This GitHub action will run your mod code through luacheck (not full support) 105 | * Auto publishing using many GitHub actions etc (see [.github/workflows/autoPublish.yml](.github/workflows/autoPublish.yml)) 106 | 107 | Additional stuff 108 | ---------------- 109 | 110 | * [FactorioSumnekoLuaPlugin](https://github.com/JanSharp/FactorioSumnekoLuaPlugin) - a plugin for the [sumneko.lua vscode extension](https://github.com/sumneko/lua-language-server) to help with factorio related syntax and intelisense. 111 | * [LuaFormatter](https://github.com/Koihik/LuaFormatter) - reformats your Lua source code (supports several editors) 112 | * Try another example with preprocessor in lua: https://github.com/ZwerOxotnik/factorio-candran-example 113 | 114 | How to start? 115 | ------------ 116 | 117 | * Read [this](https://github.com/justarandomgeek/vscode-factoriomod-debug/blob/master/workspace.md) to generate EmmyLua docs for the Factorio API properly 118 | * Change [info.json](info.json), [defines.lua](defines.lua) 119 | * Replace my nickname, this project in links, description with your stuff almost everywhere 120 | * Remove unnecessary code, files in /models, /migrations, root folder and create a file there with similar structure in the folder 121 | * **Change or delete** .github/ISSUE_TEMPLATE/* 122 | * **Change or delete** .github/workflows/* (please read [this](https://github.com/shanemadden/factorio-mod-portal-publish)) 123 | * Handle files in [control.lua](control.lua) 124 | 125 | Notes 126 | ----- 127 | 128 | * There are mods/tools that might help you (e.g.: [Factorio Library](https://mods.factorio.com/mod/flib), [Rusty's Locale Utilities](https://mods.factorio.com/mod/rusty-locale), [Big Data String Libary](https://mods.factorio.com/mod/big-data-string), [Brush tools](https://mods.factorio.com/mod/brush-tools), [Mod generator](https://github.com/ZwerOxotnik/Mod-generator) etc) 129 | * Don't restart your game if you've changed files for control stage 130 | * If you want to develop complex/big project then you'll probably try [Factorio-luacheckrc](https://github.com/Nexela/Factorio-luacheckrc) with a [GitHub action][factorio-mod-luacheck] but you have to mantain .luacheckrc file 131 | * I recommend to use [notepad++](https://notepad-plus-plus.org) when you work with data and [notepad2](https://github.com/zufuliu/notepad2) for hot fixes. For all other cases use any IDE or code editor (e.g.: [Visual Studio Code](https://code.visualstudio.com/)) 132 | * You can store data in entities to support data in blueprints (see an example in [LuaCombinator 3](https://mods.factorio.com/mod/LuaCombinator3)) 133 | * Don't add many GUIs at control stage. Factorio's GUI has the highest impact on FPS. 134 | * In Factorio, pairs() *always* iterates numerical keys 1-1024 in order (I didn't check it though) 135 | * In Factorio 1.1.71, pairs() should be more or always determinitstic ([source](https://discord.com/channels/139677590393716737/306402592265732098/1039966592219480124)) 136 | 137 | Next updates 138 | ------------ 139 | 140 | * More info about data stage 141 | * More simplification, integrations, examples 142 | * More examples of particular cases on all stages 143 | * Support of [EasyAPI](https://mods.factorio.com/mod/EasyAPI) (diplomacy, money, chat, etc) 144 | * Probably, I'll add [factorio-mod-luacheck] 145 | * Some info about [lazyAPI](https://github.com/ZwerOxotnik/zk-lib) someday 146 | * Etc 147 | 148 | Requirements 149 | ------------ 150 | 151 | Shell [scripts](./.scripts) depends on [git], [7z], [jq]. 152 | 153 | Installation on Debian and Ubuntu: 154 | 155 | ```shell 156 | sudo apt install p7zip-full jq git -y 157 | ``` 158 | 159 | Optional Dependencies 160 | --------------------- 161 | 162 | * zk-lib - for localization of [event handler](/control.lua), currently 163 | 164 | ‼️ Important Links (Translations, Discord Support) 165 | --------------------------------------------------------------- 166 | 167 | | Installation Guide | Translations | Discord | 168 | | ------------------ | ------------ | ------- | 169 | | 📖 [Installation Guide](https://wiki.factorio.com/index.php?title=Installing_Mods) | 📚 [Help with translations](https://crowdin.com/project/factorio-mods-localization) | 🦜 [Discord] | 170 | 171 | If you want to download from this source, then use commands below (requires [git]). 172 | 173 | ```bash 174 | git clone --recurse-submodules -j8 https://github.com/ZwerOxotnik/factorio-example-mod example-mod 175 | cd example-mod 176 | ``` 177 | 178 | [Contributing](/CONTRIBUTING.md) 179 | -------------------------------- 180 | 181 | Don't be afraid to contribute! We have many, many things you can do to help out. If you're trying to contribute but stuck, tag @ZwerOxotnik 182 | 183 | Alternatively, join the [Discord group][Discord] and send a message there. 184 | 185 | ~~Please read the [contributing file](/CONTRIBUTING.md) for other details on how to contribute.~~ 186 | 187 | License 188 | ------- 189 | 190 | I'm interested in distributing code as freely as possible. 191 | 192 | Copyright (c) 2021-2023 ZwerOxotnik 193 | 194 | Licensed under the [MIT licence](https://tldrlegal.com/license/mit-license). 195 | 196 | ```txt 197 | The MIT License (MIT) 198 | 199 | Permission is hereby granted, free of charge, to any person obtaining a copy 200 | of this software and associated documentation files (the "Software"), to deal 201 | in the Software without restriction, including without limitation the rights 202 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 203 | copies of the Software, and to permit persons to whom the Software is 204 | furnished to do so, subject to the following conditions: 205 | 206 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 207 | 208 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 209 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 210 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 211 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 212 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 213 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 214 | SOFTWARE. 215 | ``` 216 | 217 | [jq]: https://stedolan.github.io/jq/download/ 218 | [7z]: https://www.7-zip.org/download.html 219 | [Discord]: https://discord.gg/YyJVUCa 220 | [GitHub-page]: https://zweroxotnik.github.io/factorio-example-mod/ 221 | [git]: https://git-scm.com/downloads 222 | [factorio-mod-luacheck]: https://github.com/Roang-zero1/factorio-mod-luacheck 223 | [EmmyLua-Annotations]: https://github.com/sumneko/lua-language-server/wiki/EmmyLua-Annotations 224 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------------------------- 2 | Version: 0.6.0 3 | Date: 2023-09-07 4 | Changes: 5 | - Auto adds remote access for rcon and for other mods/scenarios 6 | - Improved commands greatly 7 | --------------------------------------------------------------------------------------------------- 8 | Version: 0.5.1 9 | Date: 2022-01-07 10 | Changes: 11 | - Improved example of settings 12 | --------------------------------------------------------------------------------------------------- 13 | Version: 0.5.0 14 | Date: 2022-01-01 15 | Changes: 16 | - Removed auto-documentation (not so needful/useful for Factorio) 17 | - Added event handler from zk-lib 18 | --------------------------------------------------------------------------------------------------- 19 | Version: 0.4.1 20 | Date: 2021-08-26 21 | Bugfixes: 22 | - Fixed "BetterCommands" 23 | --------------------------------------------------------------------------------------------------- 24 | Version: 0.4.0 25 | Date: 2021-08-19 26 | Changes: 27 | - Added some shell scripts 28 | Bugfixes: 29 | - Fixed some bugs with mod name 30 | --------------------------------------------------------------------------------------------------- 31 | Version: 0.3.5 32 | Date: 2021-08-01 33 | Changes: 34 | - Changed localization for commands 35 | Bugfixes: 36 | - Fixed crush on special scenarios 37 | --------------------------------------------------------------------------------------------------- 38 | Version: 0.3.4 39 | Date: 2021-07-11 40 | Changes: 41 | - Refactored "pre-game" stage 42 | - Added an image for the example scenario 43 | - Extended example on control stage 44 | --------------------------------------------------------------------------------------------------- 45 | Version: 0.3.3 46 | Date: 2021-07-11 47 | Changes: 48 | - Added several examples for control stage 49 | - Changed configs 50 | - Refactored some code 51 | --------------------------------------------------------------------------------------------------- 52 | Version: 0.3.2 53 | Date: 2021-07-10 54 | Changes: 55 | - Minor changes 56 | --------------------------------------------------------------------------------------------------- 57 | Version: 0.3.1 58 | Date: 2021-07-08 59 | Changes: 60 | - Added more GitHub actions 61 | --------------------------------------------------------------------------------------------------- 62 | Version: 0.3.0 63 | Date: 2021-07-08 64 | Changes: 65 | - Added auto documentation with auto publishing on GitHub pages 66 | - Improved documentation slightly 67 | - Refactored slightly 68 | - Fixed some bugs 69 | --------------------------------------------------------------------------------------------------- 70 | Version: 0.2.6 71 | Date: 2021-06-21 72 | Changes: 73 | - Added a very simple example of migration 74 | --------------------------------------------------------------------------------------------------- 75 | Version: 0.2.5 76 | Date: 2021-06-20 77 | Changes: 78 | - Minor changes 79 | - New example about data consistency 80 | --------------------------------------------------------------------------------------------------- 81 | Version: 0.2.4 82 | Date: 2021-06-18 83 | Changes: 84 | - Added example scenario 85 | - Changed examples slightly 86 | --------------------------------------------------------------------------------------------------- 87 | Version: 0.2.3 88 | Date: 2021-06-18 89 | Changes: 90 | - Minor changes 91 | - Added .gitattributes file 92 | --------------------------------------------------------------------------------------------------- 93 | Version: 0.2.2 94 | Date: 2021-06-17 95 | Changes: 96 | - Added some info 97 | - Added EmmyLua Annotations 98 | - Refactored slightly 99 | - Slightly changed control.lua 100 | --------------------------------------------------------------------------------------------------- 101 | Version: 0.2.1 102 | Date: 2021-06-14 103 | Changes: 104 | - Refactored 105 | - Improved command-wrapper 106 | Bugfixes: 107 | - Fixed switchable commands 108 | --------------------------------------------------------------------------------------------------- 109 | Version: 0.1.0 110 | Date: 2021-06-13 111 | Notes: 112 | - First release for 1.1 -------------------------------------------------------------------------------- /const-commands.lua: -------------------------------------------------------------------------------- 1 | --[[ Uses https://github.com/ZwerOxotnik/factorio-BetterCommands 2 | Returns tables of commands without functions as command "settings". All parameters are optional! 3 | Contains: 4 | name :: string: The name of your /command. (default: key of the table) 5 | description :: string or LocalisedString: The description of your command. (default: nil) 6 | is_allowed_empty_args :: boolean: Ignores empty parameters in commands, otherwise stops the command. (default: true) 7 | input_type :: string: Filter for parameters by type of input. (default: nil) 8 | possible variants: 9 | "player" - Stops execution if can't find a player by parameter 10 | "team" - Stops execution if can't find a team (force) by parameter 11 | allow_for_server :: boolean: Allow execution of a command from a server (default: false) 12 | only_for_admin :: boolean: The command can be executed only by admins (default: false) 13 | allow_for_players :: string[]: Allows to use the command for players with specified names (default: nil) 14 | max_input_length :: uint: Max amount of characters for command (default: 500) 15 | is_logged :: boolean: Logs the command into .log file (default: false) 16 | alternative_names :: string[]: Alternative names for the command (all commands should be added) (default: nil) 17 | is_one_time_use :: boolean: Disables the command after using it (default: false) 18 | is_one_time_use_for_player :: boolean: Disables for a player after using it (default: false) 19 | is_one_time_use_for_force :: boolean: Disables for a force after using it (default: false) 20 | global_cooldown :: uint: The command can be used each N seconds (default: nil) 21 | player_cooldown :: uint: The command can be used each N seconds for players (default: nil) 22 | force_cooldown :: uint: The command can be used each N seconds for forces (default: nil) 23 | disable_cooldown_for_admins :: boolean: Disables cooldown for admins (default: false) 24 | disable_cooldown_for_server :: boolean: Disables cooldown for server (default: true) 25 | ]]-- 26 | ---@type table 27 | return { 28 | } 29 | -------------------------------------------------------------------------------- /control.lua: -------------------------------------------------------------------------------- 1 | 2 | if script.level.campaign_name then return end -- Don't init if it's a campaign 3 | if script.level.level_name == "sandbox" then return end -- Don't init if it's "sandbox" scenario 4 | 5 | MAKE_DEFINE_GLOBAL = true 6 | require("defines") 7 | 8 | 9 | ---@type table 10 | local modules = {} 11 | modules.example_module = require("models/example-module") 12 | modules.data_consistency_example = require("models/data-consistency-example") 13 | -- modules.empty_module = require("models.empty-module") 14 | 15 | 16 | --- Adds https://github.com/ZwerOxotnik/factorio-BetterCommands if exists 17 | if script.active_mods["BetterCommands"] then 18 | local is_ok, better_commands = pcall(require, "__BetterCommands__/BetterCommands/control") 19 | if is_ok then 20 | better_commands.COMMAND_PREFIX = MOD_SHORT_NAME 21 | modules.better_commands = better_commands 22 | end 23 | end 24 | 25 | 26 | -- Safe disabling of this mod remotely on init stage 27 | -- Useful for other map developers and in some rare cases for mod devs 28 | if remote.interfaces["disable-" .. script.mod_name] then 29 | for _, module in pairs(modules) do 30 | local update_global_data_on_disabling = module.update_global_data_on_disabling 31 | module.events = nil 32 | module.on_nth_tick = nil 33 | module.commands = nil 34 | module.on_load = nil 35 | module.add_remote_interface = nil 36 | module.add_commands = nil 37 | module.on_configuration_changed = update_global_data_on_disabling 38 | module.on_init = update_global_data_on_disabling 39 | end 40 | else 41 | if modules.better_commands then 42 | if modules.better_commands.handle_custom_commands then 43 | -- Adds commands 44 | for _, module in pairs(modules) do 45 | modules.better_commands.handle_custom_commands(module) 46 | end 47 | end 48 | if modules.better_commands.expose_global_data then 49 | modules.better_commands.expose_global_data() 50 | end 51 | end 52 | end 53 | 54 | 55 | local event_handler 56 | if script.active_mods["zk-lib"] then 57 | -- Same as Factorio "event_handler", but slightly better performance 58 | local is_ok, zk_event_handler = pcall(require, "__zk-lib__/static-libs/lualibs/event_handler_vZO.lua") 59 | if is_ok then 60 | event_handler = zk_event_handler 61 | end 62 | end 63 | event_handler = event_handler or require("event_handler") 64 | event_handler.add_libraries(modules) 65 | 66 | 67 | -- Auto adds remote access for rcon and for other mods/scenarios via zk-lib 68 | if script.active_mods["zk-lib"] then 69 | local is_ok, remote_interface_util = pcall(require, "__zk-lib__/static-libs/lualibs/control_stage/remote-interface-util") 70 | if is_ok and remote_interface_util.expose_global_data then 71 | remote_interface_util.expose_global_data() 72 | end 73 | local is_ok, rcon_util = pcall(require, "__zk-lib__/static-libs/lualibs/control_stage/rcon-util") 74 | if is_ok and rcon_util.expose_global_data then 75 | rcon_util.expose_global_data() 76 | end 77 | end 78 | 79 | 80 | -- This is a part of "gvv", "Lua API global Variable Viewer" mod. https://mods.factorio.com/mod/gvv 81 | -- It makes possible gvv mod to read sandboxed variables in the map or other mod if following code is inserted at the end of empty line of "control.lua" of each. 82 | if script.active_mods["gvv"] then require("__gvv__.gvv")() end 83 | -------------------------------------------------------------------------------- /data-final-fixes.lua: -------------------------------------------------------------------------------- 1 | -- Prototypes: https://wiki.factorio.com/Prototype 2 | 3 | require("defines") 4 | -------------------------------------------------------------------------------- /data-updates.lua: -------------------------------------------------------------------------------- 1 | -- Prototypes: https://wiki.factorio.com/Prototype 2 | 3 | require("defines") 4 | -------------------------------------------------------------------------------- /data.lua: -------------------------------------------------------------------------------- 1 | -- Prototypes: https://wiki.factorio.com/Prototype 2 | 3 | require("defines") 4 | 5 | 6 | -- You can modify default styles 7 | local default_gui = data.raw["gui-style"].default 8 | default_gui.label.font_color = {1, 0.9, 0.9} 9 | 10 | 11 | -- Let's create new style for buttons 12 | if default_gui.invisible_example_button == nil then 13 | default_gui.invisible_example_button = 14 | { 15 | type = "button_style", 16 | font = "default-dialog-button", 17 | size = 28, 18 | padding = 4, 19 | right_margin = -6, 20 | top_margin = -3, 21 | clicked_font_color = {1, 1, 1}, 22 | hovered_font_color = {1, 1, 1}, 23 | default_graphical_set = {}, 24 | hovered_graphical_set = {} 25 | } 26 | end 27 | -------------------------------------------------------------------------------- /defines.lua: -------------------------------------------------------------------------------- 1 | -- Change data in this file in your mod! 2 | local _data = { 3 | MOD_NAME = "example-mod", 4 | MOD_PATH = "__example-mod__", 5 | 6 | -- Don't use symbols like '-' etc (it'll break pattern of regular expressions) 7 | MOD_SHORT_NAME = "em_", 8 | AUTHOR = "ZwerOxotnik" 9 | } 10 | 11 | if (not IS_DATA_STAGE and script and script.active_mods) then 12 | if not MAKE_DEFINE_GLOBAL then 13 | return _data 14 | end 15 | end 16 | 17 | --- Make content of _data global 18 | for k, v in pairs(_data) do 19 | _ENV[k] = v 20 | end 21 | -------------------------------------------------------------------------------- /info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-mod", 3 | "version": "0.6.0", 4 | "factorio_version": "1.1", 5 | "title": "Example mod", 6 | "author": "Put your nickname", 7 | "contact": "Put your contacts", 8 | "homepage": "Put a link", 9 | "description": "Put description here", 10 | "dependencies": [ 11 | "? zk-lib >= 0.10.0", 12 | "? BetterCommands", 13 | "(?) gvv" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /locale/en/example-mod.cfg: -------------------------------------------------------------------------------- 1 | # Check https://wiki.factorio.com/Tutorial:Localisation 2 | 3 | [mod-name] 4 | example-mod=Example mod 5 | [mod-description] 6 | example-mod=Lightweight modular example mod with various features and compatibilities\n\nSee source: https://github.com/ZwerOxotnik/factorio-example-mod 7 | 8 | # Custom locale format 9 | [example-mod-commands] 10 | delete-example-UI=- deletes UI of "Example mod" mod 11 | 12 | # Other possible default formats (not all) 13 | [entity-description] 14 | [entity-name] 15 | [equipment-name] 16 | [fluid-name] 17 | [virtual-signal-name] 18 | [virtual-signal-description] 19 | [item-description] 20 | [item-name] 21 | [recipe-name] 22 | [technology-name] 23 | [technology-description] 24 | [shortcut-name] 25 | [controls] 26 | [mod-setting-name] 27 | em_example_settings=Example name 28 | [mod-setting-description] 29 | em_example_settings=Example description 30 | [string-mod-setting] 31 | -------------------------------------------------------------------------------- /migrations/example-mod_0.2.6.lua: -------------------------------------------------------------------------------- 1 | -- Please, read https://lua-api.factorio.com/latest/Migrations.html 2 | 3 | for _, force in pairs(game.forces) do 4 | force.reset_recipes() 5 | force.reset_technologies() 6 | force.technologies["logistics-3"].researched = true 7 | end 8 | -------------------------------------------------------------------------------- /models/data-consistency-example.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This is just a particular example, not a guide. You don't have to follow this example at all! 3 | 4 | We gonna use https://lua-api.factorio.com/latest/Global.html to store data and some local table for singleplayer in some cases. 5 | 6 | There are some subtle differences between singleplayer and multiplayer. 7 | For instance: when a player start a map in singleplayer, the script won't use some events: 8 | defines.events.on_player_joined_game -- it works only when a player joins first time in singleplayer 9 | defines.events.on_player_left_game 10 | 11 | That leads some issues in other events. 12 | Usually, it's not a problem if data doesn't depend on those events or some related data isn't empty. 13 | We might use on_init and on_configuration_changed to always have some data. 14 | But it depends on your cases etc. 15 | 16 | So, in this example, we don't store data in global for players when they are offline and we're not going to "adapt" the data for singleplayer. 17 | ]] 18 | 19 | ---@class DataConsistencyExample : module 20 | local M = {} 21 | 22 | 23 | --#region Singleplayer data 24 | -- Usually, some tables here (it's like a mirror of global data for singleplayer) 25 | local __sp_player_data = {} 26 | --#endregion 27 | 28 | 29 | --#region Global data 30 | local _players_data 31 | --#endregion 32 | 33 | 34 | --#region Functions of events 35 | 36 | local function on_player_joined_game(event) 37 | local player_index = event.player_index 38 | local player = game.get_player(player_index) 39 | if not (player and player.valid) then return end 40 | 41 | -- Trying to simulate "loaded" game without this mod (otherwise I have to tell you about other cases) 42 | if game.is_multiplayer() then 43 | _players_data[player_index] = {} 44 | end 45 | end 46 | 47 | local function delete_player_data(event) 48 | _players_data[event.player_index] = nil 49 | end 50 | 51 | local function on_cancelled_deconstruction(event) 52 | local player_index = event.player_index 53 | local player = game.get_player(player_index) 54 | if not (player and player.valid) then return end 55 | 56 | local data = _players_data[player_index] or __sp_player_data 57 | -- You might get rid of second variable but, sometimes, it'll lead to more complex code or less performance 58 | 59 | data[#data] = math.random(100) -- it's safe to generate random numbers in multiplayer 60 | player.print("Data: " .. serpent.line(data)) 61 | -- Let's check the result 62 | if _players_data[player_index] == nil then 63 | player.print("Nothing in global data for player, using \"singleplayer\" data") 64 | else 65 | player.print("Using global to store the data") 66 | end 67 | end 68 | 69 | --#endregion 70 | 71 | 72 | --#region Pre-game stage 73 | 74 | -- You might get rid of this, but it's convenient to have and use 75 | local function link_data() 76 | _players_data = global.players 77 | end 78 | 79 | local function update_global_data() 80 | global.players = global.players or {} 81 | 82 | link_data() 83 | 84 | for player_index, _ in pairs(game.players) do 85 | global.players[player_index] = nil 86 | end 87 | end 88 | 89 | 90 | M.on_init = update_global_data 91 | M.on_configuration_changed = update_global_data 92 | M.on_load = link_data 93 | M.update_global_data_on_disabling = update_global_data -- for safe disabling of this mod 94 | 95 | --#endregion 96 | 97 | 98 | M.events = { 99 | --[defines.events.on_player_created] = on_player_created, -- it might be useful 100 | [defines.events.on_player_joined_game] = on_player_joined_game, 101 | [defines.events.on_player_left_game] = delete_player_data, 102 | [defines.events.on_player_removed] = delete_player_data, 103 | [defines.events.on_cancelled_deconstruction] = on_cancelled_deconstruction 104 | } 105 | 106 | M.commands = { 107 | -- set_spawn = set_spawn_command, -- Delete this example 108 | } 109 | 110 | 111 | return M 112 | -------------------------------------------------------------------------------- /models/empty-module.lua: -------------------------------------------------------------------------------- 1 | 2 | ---@class EmptyModule : module 3 | local M = {} 4 | 5 | 6 | --#region Global data 7 | --local _players_data 8 | --#endregion 9 | 10 | 11 | --#region Constants 12 | --local ABS = math.abs 13 | --#endregion 14 | 15 | 16 | --#region Functions of events 17 | 18 | local function on_game_created_from_scenario(event) 19 | 20 | end 21 | 22 | --#endregion 23 | 24 | 25 | --#region Pre-game stage 26 | 27 | local function link_data() 28 | --_players_data = global.players 29 | end 30 | 31 | local function update_global_data() 32 | --global.players = global.players or {} 33 | -- 34 | 35 | link_data() 36 | 37 | --for player_index, player in pairs(game.players) do 38 | -- -- delete UIs 39 | --end 40 | end 41 | 42 | 43 | M.on_init = update_global_data 44 | M.on_configuration_changed = update_global_data 45 | M.on_load = link_data 46 | M.update_global_data_on_disabling = update_global_data -- for safe disabling of this mod 47 | 48 | --#endregion 49 | 50 | 51 | M.events = { 52 | --[defines.events.on_game_created_from_scenario] = on_game_created_from_scenario, 53 | --[defines.events.on_gui_click] = on_gui_click, 54 | --[defines.events.on_player_created] = on_player_created, 55 | --[defines.events.on_player_joined_game] = on_player_joined_game, 56 | --[defines.events.on_player_left_game] = on_player_left_game, 57 | --[defines.events.on_player_removed] = delete_player_data, 58 | --[defines.events.on_player_changed_surface] = clear_player_data, 59 | --[defines.events.on_player_respawned] = clear_player_data, 60 | } 61 | 62 | M.commands = { 63 | -- set_spawn = set_spawn_command, -- Delete this example 64 | } 65 | 66 | 67 | return M 68 | -------------------------------------------------------------------------------- /models/example-module.lua: -------------------------------------------------------------------------------- 1 | 2 | ---@class ExampleModule : module 3 | local M = {} 4 | 5 | 6 | ---@type table https://lua-api.factorio.com/latest/LuaBootstrap.html#LuaBootstrap.generate_event_name 7 | local custom_events = { 8 | custom_event1 = script.generate_event_name() -- Don't use in any events, it'll cause desyncs 9 | } 10 | 11 | --#region Global data 12 | local _players_data 13 | --#endregion 14 | 15 | 16 | --#region Constants 17 | local ABS = math.abs 18 | --#endregion 19 | 20 | 21 | function say_hello() 22 | game.print("Hello") 23 | end 24 | 25 | --#region Functions of events 26 | 27 | local function on_player_created(event) 28 | local player = game.get_player(event.player_index) 29 | if not (player and player.valid) then return end 30 | 31 | global.message_for_new_player = "Events of example mod works fine" 32 | player.print(global.message_for_new_player) 33 | 34 | if player.admin then 35 | player.print("\nYou can access to mods by their \"name\" as a prefix for commands. For example: __example-mod__ global.test = 1") 36 | player.print("\nYou can use global functions from mods via console during the game.\nType it in console to try it: /c say_hello()") 37 | player.print("\nAlso, you can read and change mod global data via console during the game." .. 38 | "\nType it in console: /c __example-mod__ game.print(serpent.block(global))" 39 | ) 40 | player.print("\nAnything can use \"remote interfaces\" and I created one as \"example-mod\" " .. 41 | "(any name can be used. Usually, it uses to get custom events from a mod)," .. 42 | "\nIt's safe to use anywhere if mods handled it determinately, otherwise it'll cause desync for other players. " .. 43 | "It can improve compability with other mods etc, although there are some restrictions." .. 44 | "\nLet's try it: /c remote.call(\"example-mod\", \"say_hello\")" .. 45 | "\nAnd this: /c remote.call(\"example-mod\", \"get_event_name\", \"custom_event1\")" 46 | ) 47 | end 48 | end 49 | 50 | --#endregion 51 | 52 | 53 | --#region Commands 54 | 55 | local function delete_example_UI_command(cmd) 56 | if cmd.player_index == 0 then -- server 57 | print("Deleted UIs") 58 | else 59 | local player = game.get_player(cmd.player_index) 60 | if not player.admin then 61 | player.print({"command-output.parameters-require-admin"}) 62 | return 63 | end 64 | player.print("Deleted UIs") 65 | end 66 | 67 | for _, player in pairs(game.players) do 68 | if player.valid then 69 | -- destroy_UIs(player) 70 | end 71 | end 72 | end 73 | 74 | --#endregion 75 | 76 | 77 | --#region Pre-game stage 78 | 79 | local interface = { 80 | get_event_name = function(name) 81 | -- return custom_events[name] -- usually, it's enough 82 | game.print("ID: " .. tostring(custom_events[name])) 83 | end, 84 | say_hello = say_hello 85 | } 86 | 87 | local function add_remote_interface() 88 | -- https://lua-api.factorio.com/latest/LuaRemote.html 89 | remote.remove_interface("example-mod") -- For safety 90 | remote.add_interface("example-mod", interface) 91 | end 92 | -- You can create interface outside events 93 | -- However, the game have to "load" with the mod in order to use functions of the interface 94 | remote.add_interface("example-mod", interface) 95 | 96 | 97 | local function link_data() 98 | _players_data = global.players 99 | end 100 | 101 | local function update_global_data() 102 | global.players = global.players or {} 103 | 104 | link_data() 105 | 106 | for player_index, player in pairs(game.players) do 107 | -- delete UIs 108 | end 109 | end 110 | 111 | 112 | M.on_init = update_global_data 113 | M.on_configuration_changed = update_global_data 114 | M.on_load = link_data 115 | M.update_global_data_on_disabling = update_global_data -- for safe disabling of this mod 116 | M.add_remote_interface = add_remote_interface 117 | 118 | --#endregion 119 | 120 | 121 | M.events = { 122 | -- [defines.events.on_game_created_from_scenario] = on_game_created_from_scenario, 123 | -- [defines.events.on_gui_click] = on_gui_click, 124 | [defines.events.on_player_created] = on_player_created, 125 | -- [defines.events.on_player_joined_game] = on_player_joined_game, 126 | -- [defines.events.on_player_left_game] = on_player_left_game, 127 | -- [defines.events.on_player_removed] = delete_player_data, 128 | -- [defines.events.on_player_changed_surface] = clear_player_data, 129 | -- [defines.events.on_player_respawned] = clear_player_data, 130 | -- [defines.events.on_gui_value_changed] = on_gui_value_changed, -- please, don't use it. It impacts UPS significantly 131 | } 132 | 133 | -- M.on_nth_tick = { 134 | -- [50] = function() 135 | -- for player_index, _ in pairs(game.connected_players) do 136 | -- pcall(update_stuff, player_index) 137 | -- end 138 | -- end, 139 | -- } 140 | 141 | M.commands = { 142 | delete_example_UI = delete_example_UI_command, 143 | } 144 | 145 | 146 | return M 147 | -------------------------------------------------------------------------------- /scenarios/example/control.lua: -------------------------------------------------------------------------------- 1 | MAKE_DEFINE_GLOBAL = true 2 | require("__example-mod__/defines") -- It's possible to get lua files from other mods 3 | local event_handler = require("event_handler") 4 | 5 | ---@type table 6 | local modules = {} 7 | modules.example_module = require("models/example-module") 8 | modules.stop_example_mod = require("models/stop-example-mod") 9 | 10 | 11 | --- Adds https://github.com/ZwerOxotnik/factorio-BetterCommands if exists 12 | if script.active_mods["BetterCommands"] then 13 | local is_ok, better_commands = pcall(require, "__BetterCommands__/BetterCommands/control") 14 | if is_ok then 15 | better_commands.COMMAND_PREFIX = MOD_SHORT_NAME 16 | modules.better_commands = better_commands 17 | if better_commands.handle_custom_commands then 18 | better_commands.handle_custom_commands(modules.example_module) -- adds commands 19 | end 20 | end 21 | end 22 | 23 | 24 | event_handler.add_libraries(modules) 25 | -------------------------------------------------------------------------------- /scenarios/example/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZwerOxotnik/factorio-example-mod/a7ffb4b2ced0448ab3d446debb32709b569f1b8c/scenarios/example/image.png -------------------------------------------------------------------------------- /scenarios/example/info.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /scenarios/example/locale/en/level.cfg: -------------------------------------------------------------------------------- 1 | scenario-name=[color=orange]example[/color] 2 | description=[font=default-bold]Example[/font] -------------------------------------------------------------------------------- /scenarios/example/models/example-module.lua: -------------------------------------------------------------------------------- 1 | 2 | ---@class SExampleModule : module 3 | local M = {} 4 | 5 | 6 | --#region Global data 7 | local _players_data 8 | --#endregion 9 | 10 | 11 | --#region Constants 12 | local ABS = math.abs 13 | --#endregion 14 | 15 | function say_hi() 16 | game.print("Hi") 17 | end 18 | 19 | 20 | --#region Functions of events 21 | 22 | local function on_player_created(event) 23 | local player = game.get_player(event.player_index) 24 | if not (player and player.valid) then return end 25 | 26 | global.message_for_new_player = "Events of example scenario works fine. Also, scripts of example mod has been disabled!" 27 | player.print(global.message_for_new_player) 28 | 29 | if player.admin then 30 | player.print("\nYou can use global functions from scenarions via console during the game.\nType it in console to try it: /c say_hi()") 31 | player.print("\nAlso, you can read and change global data via console during the game.\nType it in console: /c game.print(serpent.block(global))") 32 | end 33 | end 34 | 35 | local function on_game_created_from_scenario(event) 36 | -- ######## 37 | -- Some examples of code what you can do with this event below 38 | -- Some interaction with game ignores (i.q: game.print("loaded scenario")) 39 | -- ######## 40 | 41 | -- disable_recipes() 42 | 43 | -- if global.biters_destination == nil then 44 | -- local target = game.get_entity_by_tag("target") 45 | -- global.biters_destination = target.position 46 | -- biters_destination = global.biters_destination 47 | -- if global.target_id == nil and target then 48 | -- global.target_id = script.register_on_entity_destroyed(target) 49 | -- end 50 | -- end 51 | 52 | -- local biters_spawn 53 | -- biters_spawn = game.get_entity_by_tag("biters_spawn_1") 54 | -- global.biters_spawn_position_1 = global.biters_spawn_position_1 or biters_spawn.position 55 | -- biters_spawn_position_1 = global.biters_spawn_position_1 56 | end 57 | 58 | --#endregion 59 | 60 | 61 | --#region Commands 62 | 63 | local function delete_example_UI_command(cmd) 64 | if cmd.player_index == 0 then -- server 65 | print("Deleted UIs") 66 | else 67 | local player = game.get_player(cmd.player_index) 68 | if not player.admin then 69 | player.print({"command-output.parameters-require-admin"}) 70 | return 71 | end 72 | player.print("Deleted UIs") 73 | end 74 | 75 | for _, player in pairs(game.players) do 76 | if player.valid then 77 | -- destroy_UIs(player) 78 | end 79 | end 80 | end 81 | 82 | --#endregion 83 | 84 | 85 | --#region Pre-game stage 86 | 87 | local function link_data() 88 | _players_data = global.players 89 | end 90 | 91 | local function update_global_data() 92 | global.players = global.players or {} 93 | 94 | link_data() 95 | 96 | for player_index, player in pairs(game.players) do 97 | -- delete UIs 98 | end 99 | end 100 | 101 | 102 | M.on_init = update_global_data 103 | M.on_configuration_changed = update_global_data 104 | M.on_load = link_data 105 | M.update_global_data_on_disabling = update_global_data -- for safe disabling of this mod 106 | 107 | --#endregion 108 | 109 | 110 | M.events = { 111 | [defines.events.on_game_created_from_scenario] = on_game_created_from_scenario, 112 | -- [defines.events.on_gui_click] = on_gui_click, 113 | [defines.events.on_player_created] = on_player_created, 114 | -- [defines.events.on_player_joined_game] = on_player_joined_game, 115 | -- [defines.events.on_player_left_game] = on_player_left_game, 116 | -- [defines.events.on_player_removed] = delete_player_data, 117 | -- [defines.events.on_player_changed_surface] = clear_player_data, 118 | -- [defines.events.on_player_respawned] = clear_player_data, 119 | -- [defines.events.on_gui_value_changed] = on_gui_value_changed, -- please, don't use it. It impacts UPS significantly 120 | } 121 | 122 | -- M.on_nth_tick = { 123 | -- [50] = function() 124 | -- for player_index, _ in pairs(game.connected_players) do 125 | -- pcall(update_stuff, player_index) 126 | -- end 127 | -- end, 128 | -- } 129 | 130 | 131 | M.commands = { 132 | delete_example_UI = delete_example_UI_command, 133 | } 134 | 135 | 136 | return M 137 | -------------------------------------------------------------------------------- /scenarios/example/models/stop-example-mod.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Safely disables another mod/script remotely on init stage 3 | Useful for other map developers and in some rare cases for mod devs 4 | But another mod must have such interface. 5 | 6 | See control.lua in the mod for other details. 7 | ]] 8 | 9 | ---@class StopExampleMod : module 10 | local M = {} 11 | local mod_name = "disable-" .. MOD_NAME 12 | 13 | remote.remove_interface(mod_name) 14 | remote.add_interface(mod_name, {}) -- trying to disable another mod 15 | 16 | 17 | -- Reversly using this method :) 18 | -- Remove garbage 19 | M.add_remote_interface = function() 20 | remote.remove_interface(mod_name) 21 | mod_name = nil 22 | end 23 | 24 | 25 | return M 26 | -------------------------------------------------------------------------------- /settings-final-fixes.lua: -------------------------------------------------------------------------------- 1 | -- See https://wiki.factorio.com/Tutorial:Mod_settings#Reading_settings 2 | 3 | require("defines") 4 | -------------------------------------------------------------------------------- /settings-updates.lua: -------------------------------------------------------------------------------- 1 | -- See https://wiki.factorio.com/Tutorial:Mod_settings#Reading_settings 2 | 3 | require("defines") 4 | -------------------------------------------------------------------------------- /settings.lua: -------------------------------------------------------------------------------- 1 | -- See https://wiki.factorio.com/Tutorial:Mod_settings#Reading_settings 2 | 3 | require("defines") 4 | 5 | 6 | --- Adds settings for commands 7 | if mods["BetterCommands"] then 8 | local is_ok, better_commands = pcall(require, "__BetterCommands__/BetterCommands/control") 9 | if is_ok then 10 | better_commands.COMMAND_PREFIX = MOD_SHORT_NAME 11 | better_commands.create_settings(MOD_PATH, MOD_SHORT_NAME) -- Adds switchable commands 12 | end 13 | end 14 | 15 | 16 | -- Just an example 17 | -- https://wiki.factorio.com/Tutorial:Mod_settings#Creation 18 | -- data:extend({ 19 | -- { 20 | -- type = "int-setting", 21 | -- name = "my-unique-setting", 22 | -- setting_type = "startup", -- or runtime-global or runtime-per-user 23 | -- minimum_value = 1, 24 | -- maximum_value = 1000, 25 | -- default_value = 100, 26 | -- localised_name = {"Example"}, -- Optional 27 | -- localised_description = {"Example"}, -- Optional 28 | -- order = "any-order", -- Optional 29 | -- hidden = false -- Optional (false by default) 30 | -- } 31 | -- }) 32 | -------------------------------------------------------------------------------- /special-info.md: -------------------------------------------------------------------------------- 1 | ## Please, always do this: 2 | ```lua 3 | local DESTROY_SETTINGS = {raise_destroy=true} 4 | entity.destroy(DESTROY_SETTINGS) 5 | ``` 6 | ## Instead of this: 7 | ```lua 8 | entity.destroy() 9 | ``` 10 | Otherwise, you'll break many mods. 11 | 12 | # 13 | 14 | ## This is faster than 15 | ```lua 16 | /measured-command 17 | local tiles = {} 18 | local c = 0 19 | for i = -1000, 1000 do 20 | for j = -1000, 1000 do 21 | c = c + 1 22 | tiles[c] = {position = {i, j}, name = "refined-concrete"} 23 | if c > 1024 then 24 | game.player.surface.set_tiles(tiles, false, false, false) 25 | c = 0 26 | end 27 | end 28 | end 29 | game.player.surface.set_tiles(tiles, false, false, false) 30 | ``` 31 | 32 | ## this 33 | ```lua 34 | /measured-command 35 | local tiles = {} 36 | local c = 0 37 | for i = -1000, 1000 do 38 | for j = -1000, 1000 do 39 | c = c + 1 40 | tiles[c] = {position = {i, j}, name = "refined-concrete"} 41 | end 42 | end 43 | game.player.surface.set_tiles(tiles, false, false, false) 44 | ``` 45 | 46 | So could use it with [surface.clone_area()](https://lua-api.factorio.com/latest/LuaSurface.html#LuaSurface.clone_area) to make things even faster (it takes a lot of time). 47 | -------------------------------------------------------------------------------- /switchable-commands.lua: -------------------------------------------------------------------------------- 1 | --[[ Uses https://github.com/ZwerOxotnik/factorio-BetterCommands 2 | Returns tables of commands without functions as command "settings". All parameters are optional! 3 | Contains: 4 | name :: string: The name of your /command. (default: key of the table) 5 | description :: string or LocalisedString: The description of your command. (default: nil) 6 | is_allowed_empty_args :: boolean: Ignores empty parameters in commands, otherwise stops the command. (default: true) 7 | input_type :: string: Filter for parameters by type of input. (default: nil) 8 | possible variants: 9 | "player" - Stops execution if can't find a player by parameter 10 | "team" - Stops execution if can't find a team (force) by parameter 11 | allow_for_server :: boolean: Allow execution of a command from a server (default: false) 12 | only_for_admin :: boolean: The command can be executed only by admins (default: false) 13 | is_added_by_default :: boolean: Default value for switchable commands (default: true) 14 | allow_for_players :: string[]: Allows to use the command for players with specified names (default: nil) 15 | max_input_length :: uint: Max amount of characters for command (default: 500) 16 | is_logged :: boolean: Logs the command into .log file (default: false) 17 | alternative_names :: string[]: Alternative names for the command (all commands should be added) (default: nil) 18 | is_one_time_use :: boolean: Disables the command after using it (default: false) 19 | is_one_time_use_for_player :: boolean: Disables for a player after using it (default: false) 20 | is_one_time_use_for_force :: boolean: Disables for a force after using it (default: false) 21 | global_cooldown :: uint: The command can be used each N seconds (default: nil) 22 | player_cooldown :: uint: The command can be used each N seconds for players (default: nil) 23 | force_cooldown :: uint: The command can be used each N seconds for forces (default: nil) 24 | disable_cooldown_for_admins :: boolean: Disables cooldown for admins (default: false) 25 | disable_cooldown_for_server :: boolean: Disables cooldown for server (default: true) 26 | ]]-- 27 | ---@type table 28 | return { 29 | delete_example_UI = {name = "delete-example-UI"}, -- See models/example-module.lua 30 | } 31 | -------------------------------------------------------------------------------- /thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZwerOxotnik/factorio-example-mod/a7ffb4b2ced0448ab3d446debb32709b569f1b8c/thumbnail.png --------------------------------------------------------------------------------