NOTE: for LLMs, not humans.
This document outlines the coding conventions and style guidelines for the FactorioAccess mod codebase. Following these guidelines ensures consistency and maintainability across the project.
- Use snake_case for all function names
- Functions that read/announce information should start with
read_orreport_ - Functions that check conditions should start with
is_orhas_ - Event handlers should be prefixed with
kb_for keybindings or use descriptive names
-- Good
function mod.read_entity_info(pindex, entity)
function mod.is_pipe_connected(entity)
function kb_pause_menu(event)
-- Bad
function readEntityInfo(pindex, entity)
function checkPipeConnection(entity)- Use snake_case for all variables
- Local variables should be declared with
local - Player index should always be named
pindex - Player object should be named
porplayer - Avoid single letter variables except for common patterns (i, j for loops)
-- Good
local pindex = event.player_index
local player = game.get_player(pindex)
local entity_count = 0
-- Bad
playerIndex = event.player_index
local plr = game.get_player(pindex)
local e = 0- Constants can use UPPER_CASE but this is not consistently enforced
- Prefer descriptive names over abbreviations
-- Acceptable patterns
local TICK_RATE = 60
local dirs = defines.direction -- Common alias- ALWAYS use the local module pattern, never global modules
- Module variable should be named
mod - Return the module at the end of the file
-- CORRECT: Local module pattern
local mod = {}
function mod.some_function()
-- implementation
end
return mod
-- WRONG: Global module pattern (DO NOT USE)
MyModule = {} -- This pollutes global namespace!- Start with requires at the top
- Define local shortcuts (like
dirs = defines.direction) - Create module table
- Define module functions
- Register event handlers (preferably through EventManager)
- Return module at end
-- Standard file structure
local util = require("util")
local fa_utils = require("scripts.fa-utils")
local dirs = defines.direction
local mod = {}
-- Module functions
function mod.do_something(pindex)
-- implementation
end
-- Event handlers (new pattern - use EventManager)
local EventManager = require("scripts.event-manager")
EventManager.on_event(defines.events.on_built_entity, function(event)
-- handler
end)
return mod- Group related functions together
- Add section comments for major functionality groups
- Keep functions focused on a single responsibility
- Use table literals when possible
- For complex tables, build incrementally
- Use
table.insertfor dynamic arrays
-- Simple table literal
local data = {
name = "test",
value = 42,
items = {"a", "b", "c"}
}
-- Building complex tables
local result = {}
for i, item in pairs(items) do
table.insert(result, process_item(item))
end- Always validate player and entity objects
- Check
validproperty before using entities - Return early on invalid conditions
function mod.process_entity(pindex, entity)
local player = game.get_player(pindex)
if not (player and player.valid) then return end
if not (entity and entity.valid) then
return "Invalid entity"
end
-- Process entity
end- Use explicit nil checks
- Combine validity checks with logical operators
- Provide fallback values where appropriate
-- Good nil checking patterns
if entity ~= nil and entity.valid then
-- use entity
end
local count = stack.count or 0
local name = entity.name or "unknown"- Use
pairsfor tables,ipairsfor arrays - Cache table lookups outside loops when possible
- Avoid creating tables inside hot loops
-- Good loop patterns
local players_data = storage.players
for pindex, player_data in pairs(players_data) do
-- process player data
end
-- Avoid table creation in loops
-- Bad:
for i = 1, 100 do
local pos = {x = i, y = i} -- Creates new table each iteration
end
-- Good:
local pos = {}
for i = 1, 100 do
pos.x = i
pos.y = i
-- use pos
end- Always pass
pindexas the first parameter to functions - Never assume single player - always use pindex
- Access player data through storage.players[pindex]
function mod.read_info(pindex, extra_param)
local player = game.get_player(pindex)
local player_data = storage.players[pindex]
-- implementation
end- Use MessageBuilder for complex localized messages
- For simple messages, use locale string arrays
- Never hardcode user-facing strings
-- Modern pattern with MessageBuilder
local message = MessageBuilder.new()
message:fragment({"entity-name.transport-belt"})
message:fragment({"fa.ent-info-facing", direction_lookup(entity.direction)})
Speech.speak(pindex, message:build())
-- Simple pattern for basic messages
local result = {"",
{"entity-name.transport-belt"},
" facing ",
direction_lookup(entity.direction)
}
Speech.speak(pindex, result)- Use descriptive sound paths
- Always play sounds through the player object
- Common sounds: "Inventory-Move", "inventory-edge", "utility/cannot_build"
- When possible go through or add functions to scripts/ui/sounds.lua
-- Standard sound playing
player.play_sound({ path = "Inventory-Move" })
player.play_sound({ path = "utility/cannot_build" })- All user-facing strings must use the locale system
- Use locale keys like "fa.message-key" for mod-specific strings
- Use {"entity-name.entity"} for game entity names
-- Good localization
Speech.speak(pindex, {"fa.building-info", entity.name})
-- Bad - hardcoded string
Speech.speak(pindex, "Building: " .. entity.name) -- DO NOT DO THIS- Always add LuaLS type annotations for function parameters
- Document return types
- Use proper type names from Factorio API
---@param pindex number Player index
---@param entity LuaEntity Entity to process
---@param check_connection boolean? Optional connection check
---@return string Result message
function mod.process_entity(pindex, entity, check_connection)
-- implementation
end- Add descriptive comments before complex functions
- Explain the purpose, not just what the code does
- Document any side effects
--[[
Scans for entities in a circular pattern around the player.
Updates the scanner cache and announces the closest entity found.
Note: This modifies storage.players[pindex].scanner_data
]]
function mod.scan_around_player(pindex, radius)
-- implementation
end- Use consistent markers:
--TODO:,--FIXME:,--laterdo - Include description of what needs to be done
- Reference issue numbers when available
--TODO: Implement proper error handling for multiplayer
--FIXME: This crashes when entity becomes invalid between ticks
--laterdo: Add localization for this message- NEVER use global variables except through storage
- NEVER use
_G.variable = value- this is cheating - Always use local variables and module pattern
-- WRONG - Global pollution
MyGlobalVar = 42 -- DO NOT DO THIS
_G.some_function = function() end -- DO NOT DO THIS
-- CORRECT
local my_var = 42
local mod = {}
function mod.some_function() end- Avoid hardcoded numbers without explanation
- Use named constants or add comments
- Exception: Common tick intervals (60 = 1 second)
-- Bad
if event.tick % 15 == 0 then -- What is 15?
-- Good
local SCANNER_UPDATE_INTERVAL = 15 -- Update every 0.25 seconds
if event.tick % SCANNER_UPDATE_INTERVAL == 0 then
-- Acceptable with comment
if event.tick % 60 == 0 then -- Every second- Never hardcode user-facing messages
- Always use the localization system
- Exception: Internal debug messages with game.print()
-- WRONG
Speech.speak(pindex, "Building placed at " .. pos.x .. ", " .. pos.y)
-- CORRECT
Speech.speak(pindex, {"fa.building-placed", pos.x, pos.y})- Don't test Factorio's behavior - test your mod's functionality
- Don't verify API contracts in tests
- Focus on your mod's logic and state management
-- Bad test - tests Factorio API
it("should create valid entity", function(ctx)
local entity = surface.create_entity{...}
ctx:assert(entity.valid) -- Testing Factorio, not your mod
end)
-- Good test - tests mod behavior
it("should add entity to tracking list", function(ctx)
local entity = surface.create_entity{...}
mod.track_entity(1, entity)
ctx:assert_equals(1, #storage.players[1].tracked_entities)
end)- Requires only work at file top-level
- Never use require inside functions - it will crash
-- WRONG - Will crash at runtime
function mod.load_module()
local other = require("scripts.other") -- CRASH!
end
-- CORRECT - At top level only
local other = require("scripts.other")
function mod.use_module()
other.do_something()
end- Spread expensive operations across multiple ticks
- Use modulo operators for periodic checks
- Document tick intervals
-- Common patterns
if event.tick % 15 == 0 then -- 4 times per second
-- medium frequency updates
end
if event.tick % 60 == 0 then -- Once per second
-- low frequency updates
end- Cache frequently accessed values
- Cache global lookups in local variables
- Invalidate caches when data changes
-- Cache globals locally
local floor = math.floor
local insert = table.insert
-- Cache repeated lookups
function mod.process_entities(entities)
local player_data = storage.players[pindex]
for _, entity in pairs(entities) do
-- use player_data instead of repeated storage access
end
endThe codebase is transitioning to centralized event management. New code should use EventManager:
-- NEW PATTERN - Use this for new code
local EventManager = require("scripts.event-manager")
EventManager.on_event(defines.events.on_built_entity, function(event)
-- handler code
end)
-- OLD PATTERN - Being phased out
script.on_event(defines.events.on_built_entity, function(event)
-- handler code
end)Following these guidelines helps maintain a consistent, readable, and performant codebase. When in doubt:
- Look at well-written modules like fa-utils.lua, message-builder.lua, and storage-manager.lua
- Keep accessibility in mind - this mod serves blind players
- Test with actual screen readers when possible
- Ask for clarification rather than assuming