├── .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 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
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
--------------------------------------------------------------------------------