WARFRAME Wiki
Advertisement
WARFRAME Wiki

Similar to WARFRAME Wiki:Styling Guide for article pages, this article aims to establish a coding convention for Lua scripts and tables in Module pages for consistency, readability, maintainability, and ease of editing. As this wiki is a community-based and open-source project, it is expected that contributors follow these guidelines and best practices to ensure the longevity of the wiki's codebase. Note that these guidelines are not strictly enforced so feel free to deviate from them if it feels appropriate to to do so.

Last updated: Sun, 13 Jun 2021 20:02:10 +0000 (UTC) by User:Cephalon Scientia


Style Guide

Modules and submodules should generally follow conventions established by lua-users.org with some changes since these scripts are written in the context of running on a MediaWiki-based wiki.

Above all styling guidelines: Be consistent, write readable code, and follow the convention established within the module you are editing.

Formatting

  • Indentation should use tabs. Indentation must be consistent throughout the entire module.
    • Stick to using one indentation character thoughout the module (i.e. tabs or spaces, preferably tabs however).
  • Try to limit statement lengths within 80 characters for visibility. If line wrapping is absolutely necessary, indent the next statement to make it clear that a statement is line wrapping instead of being two separate statements.
    • For strings that exceed this limit use multi-line string tokens:
      local str = [[
          This is a
          multi-line
          string.
      ]]
      
      -- Alternatively, if your string value contains 
      -- "[[" and "]]" then expand it with equal signs:
      local str = [=[ 
          This is a multi-line string, but
          it has [[ something with square brackets around it ]].
          This is needed when denoting wikitext links in
          multi-line strings like: [[WARFRAME]].
      ]=]
      
  • Semicolons at the end of statements are optional. Most of the time they are not used, but if they are, use it throughout the module.
  • When denoting strings, stick to one quote style throughout the module (i.e. single quotations ' or double quotations ").
    • A valid exception would be if a string represents tags in wikitext with CSS attributes (e.g. local result = '<div style="text-align:center;">Test</div>').
  • Avoid writing "one-liners" which are code blocks that are compressed into one line. These are often times hard to follow and maintain, especially with nested delimiters. Instead, expand these onto multiple lines with proper indentation.
    -- Instead of writing this:
    local function printAndIncrement(t) for k, v in pairs(t) do print(v); t[k] = v + 1 end return t end
    
    -- Write:
    local function printAndIncrement(t) 
        for k, v in pairs(t) do 
            print(v)
            t[k] = v + 1 
        end
        return t
    end
    
  • Add a single space after commas (,) and semicolons (;) before continuing the code statement for readability.
  • Add a single space between two different brackets/parentheses for readability, especially within the source editor.
    -- Instead of writing this:
    {returnsAFunctionTable()["functionName"]({key = "value"})}
    
    -- Write:
    { returnsAFunctionTable()["functionName"]( { key = "value" } ) }
    
    -- Or:
    { returnsAFunctionTable() ["functionName"] ( { key = "value" } ) }
    
Example
-- As seen in Module:Tooltips/tip, maps shorthand polarity name to polarity image.
-- Do not program everything like this normally. In this scenario, the code is still
-- readable, just not at first glance due to nested brackets.
function getPolarityImage(pol)
    return ({
        Zenurik = "Zenurik_Pol_W.png",
        Naramon = "Naramon_Pol_W.png",
        Vazarin = "Vazarin_Pol_W.png",
        Penjaga = "Penjaga_Pol_W.png",
        Madurai = "Madurai_Pol_W.png",
        Koneksi = "Koneksi_Pol_W.png",
        Umbra = "Umbra_Pol_W.png",
        Unairu = "Unairu_Pol_W.png",
        Exilus = "Exilus_W.png",
        Aura = "Aura_Pol_White.png",
        None = "Spacer.png",
    })[({
        Ability = 'Zenurik',
        Bar = 'Naramon',
        D = 'Vazarin',
        Y = 'Penjaga', Sila = 'Penjaga', Sentinel = 'Penjaga', Precept = 'Penjaga',
        V = 'Madurai',
        O = 'Koneksi', Core = 'Koneksi', Fusion = 'Koneksi', Pengetikan = 'Koneksi',
        U = 'Umbra', Q = 'Umbra',
        Ward = 'Unairu', R = 'Unairu',
    })[pol]]
end
  • Add a single space before and after equal signs (=) when assigning variables.
  • Stick to one way of using mw.loadData() and require when importing modules/packages/libraries. This guideline also applies to all function calls with a single string or table argument:
    -- Using standard function notation
    local Math = mw.loadData('Module:Math')
    local Math = require('Module:Math')
    
    -- Syntactic sugar:
    -- Omitting optional parentheses in function call
    local Math = mw.loadData 'Module:Math'
    local Math = require 'Module:Math'
    
    -- Syntactic sugar:
    -- Omitting optional parentheses and using multi-string tokens
    -- to make argument look like an interwiki link
    local Math = mw.loadData [[Module:Math]]
    local Math = require [[Module:Math]]
    
  • Take advantage of Lua's string.format() for a feature similar to string interpolation. Use this over concatenating many string values as it makes code easier to read and maintain:
    -- Instead of writing this:
    function buildSimpleGallery(image, link, caption)
        local gallery = '<gallery widths="200" hideaddbutton="true" captionalign="center" spacing="small" bordercolor="transparent">\n'
        local galleryEntry = image.."|link="..link.."|''"..caption.."''"
        return gallery..galleryEntry..'\n</gallery>'
    end
    
    -- Write:
    function buildSimpleGallery(image, link, caption)
        local gallery = '<gallery widths="200" hideaddbutton="true" captionalign="center" spacing="small" bordercolor="transparent">\n'
        -- Variable values will replace "%s" placeholder tokens
        local galleryEntry = string.format("%s|link=%s|''%s''", image, link, caption)
        -- Alternatively: ("%s|link=%s|''%s''").format(image, link, caption)
        return gallery..galleryEntry..'\n</gallery>'
    end
    
  • Avoid making many simple if/else code blocks for variable assignments or return statements. These can be condensed using and/or as ternary operations.
    • Take advantage of short circuiting techniques when doing so.
      -- Instead of writing this:
      function getArgFromFrame(frame)
          local arg1 = ""
          -- Alternatively: if not (frame and frame.args and frame.args[1]) then
          if frame == nil or frame.args == nil or frame.args[1] == nil then
              arg1 = "Placeholder"
          else
              arg1 = frame.args[1]
          end
          print("First argument:", arg1)
          return arg1
      end
      
      -- Write:
      function getArgFromFrame(frame)
          -- Interpret this as if either frame or frame.args are not nil,
          -- use frame.args[1] value; 
          -- if frame.args[1] value is nil, use "Placeholder";
          -- if frame or frame.args are nil, use "Placeholder"
          local arg1 = ((frame and frame.args) and frame.args[1]) or "Placeholder"
          print("First argument:", arg1)
          return arg1
      end
      
  • Module imports should come before any other code statements or blocks and should exist at the very top of the function for readability and maintainability. Putting require() or mw.loadData() within functions may help with performance if they are only called zero or one times, but generally it is better to just import everything at once before calling functions of those modules.

Naming

  • Value and object (table) variable naming — Keep variable names as consise yet descriptive as possible. Use camelCasing if variable name has more than one word.
    • For booleans variables, name them according to the state they represent.
    • p is used to denote a package holding all the public/exported functions of a module.
    • Avoid using reserved keywords (e.g. local or function) as variable names.
    • Avoid using package/library names (e.g. math or table) as variable names.
    • The variable consisting of only an underscore ("_") is commonly used as a placeholder when you want to ignore the variable:
      for _, v in ipairs(t) do 
          print(v)
      end
      
    • i, k, v, and t (representing "index", "key", "value", and "table" respectively) are often used as follows:
      for k, v in pairs(t) <content> end
      for i, v in ipairs(t) <content> end
      mt.__newindex = function(t, k, v) <content> end
      
    • If tables are treated as objects, storing state (key-value pairs) and/or behavior (functions), then they are usually in PascalCase.
      -- No need to capitalize, simple list
      local list = { "This", "is", "a", "list" }
      
      -- Capitalize the variable name since it has key-value pairs
      -- and a function
      local DataEntry = {
          name = "Test",
          isActive = true,
          printElem = function(elem) print(elem) end
      }
      
      -- Importing other modules w/ exported functions
      local Math = require("Module:Math")
      
  • Function naming — Functions follow similar rules to value and object variable naming, being first class objects. Function names use camelCase and should start with a verb (e.g. getValue() or buildTable()).
    • Functions that cannot be {{#invoke:}}d on article pages should start with a single underscore (e.g. _getValue()) to distinguish them for functions that use the parser frame and frame.args to grab arguments from the {{#invoke:}} parser function. These functions are typically used in other modules.
  • Lua internal variable naming — By convention, names starting with an underscore followed by uppercase letters (such as _VERSION) are reserved for internal global variables used by Lua.
  • Metamethods naming — By convention, names starting with double underscores (e.g. __add) are used to denote metamethods which changes how Lua performs an operation on a particular item (such as defining how to add two tables).
  • Constants naming — By convention, constants are named in SCREAMING_SNAKE_CASE, with words separated by underscores. Lua does not natively support immutable variables so either trust that end—users do not modify constants or put them in a read-only table.
  • Module/package/library naming — module names use PascalCasing such as Module:DamageTypes.
    • Submodules should reside as a subpage of the main module page (e.g. Module:PAGENAME/<submodule>).
    • Databases in the form of Lua tables should reside in the /data subpage (e.g. Module:PAGENAME/data).
    • Test cases should reside in the /testcases subpage (e.g. Module:PAGENAME/testcases) to be recognized by Module:TestHarness.
  • self keyword refers to the object a method is invoked on (like this in C++ or Java). This is enforced by the colon (:) syntactic sugar.
    • Most likely you will not see this paradigm in our modules.
      local Car = { position = 10 }
      
      function Car:move(distance)
        self.position = self.position + distance
      end
      
      -- The above function is equilvalent to the following code snippet:
      function Car.move(self, distance)
        self.position = self.position + distance
      end
      

Commenting

  • Every commented text should have space after the inline comment delimiter (--).
  • Every module and exported function should have at least one line of comments describing its use or purpose. Though we prefer you follow LuaDoc conventions for our document generator Module:Docbunto. Note that Docbunto code items are introduced by a block comment (--[[ ]]--), an inline comment with three hyphens (---), or an inline @tag comment.
    --- '''Math''' is an extension of the math STL, containing additional functionality 
    --  and support.
    --  
    --  On this Wiki, Math is used in:
    --  * [[Module:Acquisition]]
    --  * [[Module:Shared]]
    --  * [[Module:Warframes]]
    --  * [[Module:Weapons]]
    --  * [[Template:Math]]
    --  
    --  @module		math
    --  @alias		p
    --  @author		[[User:FINNER|FINNER]]
    --  @image		MathLogo.png
    --  @require	[[w:c:dev:Module:Arguments|Module:Arguments]]
    --  @require	[[w:c:dev:Module:Entrypoint|Module:Entrypoint]]
    --  @require	[[w:c:dev:Module:User error|Module:User error]]
    --  @release	stable
    --  <nowiki>
    
    --- Evaluates input
    --  @function		p.eval
    --  @param			{string} num The input expression
    --  @return			{number} eval(num)
    function p.eval(...)
    	local args = getArgs({...}, {noEval = true});
    	if not(args[1]) then return userError('Not enough input arguments | eval(num)'); end
        return eval(args[1]);
    end
    

Development Standards

Design

Functions

  • Functions should have one job and one job only. Break up larger functions into smaller ones whenever possible for modularity and reusability.
  • Use existing functions instead of remaking new ones with the same functionality by importing existing modules/packages/libraries through mw.loadData() or require keyword.
  • Keep function definitions below 5 parameters. If you need more arguments, there are two ways to approach this problem using unpack():
    • Pass in an argument table containing all of your arguments.
      function add(frame)
        -- frame takes on the form { <metadata>, args = { <arguments> } }
        -- if passing arguments through {{#invoke:}}
        local x, y = unpack(frame.args)
        return x + y
      end
      
    • Add ... to the end of argument list to denote a variadic or vararg function.
      function add(...)
        -- unpack() returns a tuple of arguments
        local x, y = unpack({...})
        return x + y
      end
      
    • See https://www.lua.org/pil/5.1.htmlf for more information.
  • Exported functions will almost always have the frame object whose arguments passed through by {{#invoke:}} can be accessed in the following table: frame.args.
  • If possible, return the modified original input argument to support function chaining for local and/or exported functions.
  • Limit the use of recursive functions or extremely long loops in modules. These scripts have limited allocation time and memory to execute, otherwise an error like Lua error: the time allocated for running scripts has expired or Lua error: not enough memory will be shown.
  • Avoid creating nested functions above one level. In other words, (lambda or anonymous) functions in a function should not define new functions in them.
  • When building wikitext to be displayed on an article, be sure to add a mw.log() that outputs the resultant wikitext. This is for wiki maintainers to copy and paste the wikitext in archived pages where a module may not support them anymore.
  • Use Module:Arguments to handle arguments that are passed in via templates that use {{#invoke:}}. The library sanitizes arguments and removes empty strings that are typically used as default arguments when no value is passed in for a parameter.
    local Args = require('Dev:Arguments')
    
    function example(frame)
        local sanitizedArgs = Args.getArgs(frame.args or frame)
    end
    
  • Surround a function with parentheses (( )) if you want to only use the first argument that it returns.
  • Limit nested code blocks to three at max. This includes for loops and if/else blocks. Any longer nested code blocks can usually be refactored into different helper functions or combined into a singular code block for conditional checks.
  • Whenever possible, always use native functions from modules rather than relying on frame:preprocess() to expand wikitext (e.g. templates, parser functions, and parameters such as {{{1}}} in string results. This is to improve function performance.

Modules

  • Modules should only pertain to one type of functionality and have limited scope. Generic functions should be relocated to Module:Shared, Module:Math, Module:String, or Module:Table.
  • Modules typically follow a Module-Template design pattern where a Template page invokes functions from the module and article pages will transclude templates to display information generated by modules.
    • Examples include Module:Math/Template:Math and Module:Tooltips/Template:Tooltip
    • This is to decouple articles from modules in a separations of concern design. If an {{#invoke:}} call needs to be updated due to changes to implementation (e.g. moving functions to different modules or new function signature), we will not have to replace all {{#invoke:}} calls that exist on pages. A single update to the {{#invoke:}} calls in the desired template is sufficient for site-wide changes.
  • Most of the modules follow Object-oriented programming (OOP) techniques and convention. Typically, OOP increases modularity and reusability of code, making codebases easier to manage.
    • The modules themselves can be considered as objects when imported in other modules, however, they cannot store state and are immutable.
    • Typically, we do not explicitly define classes (using tables and metatables in Lua) mostly because of legacy code not being written to support that. Often times class-like structures are not needed as all of our modules store functions and state is stored in /data subpages.
  • Use local scope whenever possible. Too many global variables can potentially cause problems and add complexity, especially when modified and used in different functions.

Databases (/data)

  • All of our /data subpages contain Lua tables that store values in key-value pairs. They are structured like NoSQL key-value store databases.
  • All key names should be in TitleCase.
  • Values should be one type for a particular key name for consistency. Note that nil values are okay.
  • Keys that store article names or interwiki links should only have the full name of the article with no additional wikitext. If you want to add additional formatting, that would be done in the modules or templates.
    local t = {
        Image = "Panel.png",  -- Instead of "[[File:Panel.png]]" or "File:Panel.png|30px"
        Link = "Damage/Impact Damage"  -- Instead of "[[Damage/Impact Damage]]" or "Damage/Impact Damage|Impact"
    }
    

Architecture

General architecture of module pages:

M:<name> — invokable by articles/the main module
M:<name>/testcases — unit tests

M:<name>/data — Lua table database (optional)
M:<name>/data/validate — data validation script/unit tests (required if /data exists)

M:<name>/dev  — development/sandbox (optional)
M:<name>/<additional_subpages> — misc submodules (optional)

Most, if not all, pages/subpages will have /doc for documentation

Documentation

  • Each module and submodule has their own /doc subpage that will automatically be transcluded in the relevant module or submodule.
  • Every function should have a LuaDoc-style comment before them, describing the function's functionality, parameters, and what the function returns.
  • Unclear or unintuitive code statements/blocks should have a comment before them describing their purpose or why they are needed. Use your best judgment when adding these, not everything needs to be commented.
  • If documenting problems with the code or features that you want to see, please add TODO comments in the desired location.

Testing

  • Each module and submodule has their own /testcases subpage that will contain a Lua table with test parameters and expected outputs.
  • Each publicly exposed function should have at least one test to prove its intended functionality.

Error Handling

  • Use assert(expression, message) and error(message [, level]) to handle runtime errors. Be sure to output descriptive text so users could understand what they are doing incorrectly when invoking a function or calling a template that uses functions from modules. Error messages should also specify what function is returning that message and the arguments that are passed for easier debugging.
    • These functions will return a stack traceback which is useful for debugging purposes.
    • Errors will be in the style of bolded, red text.
      -- Example usage of assert() and error():
      function findElem(t, search)
          assert(type(t) ~= "table", "printArg(t): argument has to be a table")
          for _, elem in ipairs(t) do
              if elem == search then
                  return true
              end
          end
          error("printArg(t): cannot find element "..search.." in table")
      end
      
  • If you want code to continue running after running into an error (similar to a try/catch block), use protected call pcall(function, arg1, ...). When an error occurs, pcall() will return just the status of the error.
    • Do not use this unless absolute necessary. Having too many protected calls makes it difficult to debug when there are functionality issues since errors are silently handled.
  • If you want to set an error handler, use xpcall(function, error handler function). You have to wrap your desired function call in another function that takes in no arguments.

Internationalization

Photo-4
“It's taking longer than I calculated.”
This page is actively being worked on and may not be completely correct. Please assist in making this page accurate. See WARFRAME Wiki:Research on ways to perform research on this game. Click here to add more info.
Nothing in development or planning for i18n and i10n for the wiki as of 03:39, 31 May 2021 (UTC). This is not high on priority and would be a huge epic to tackle, estimating a 6-12 month duration as an on-and-off project. Feel free to contact User:Cephalon Scientia if you have some ideas or desire such a feature. Right now, our sister wikis just fork from this wiki's codebase and either 1. change the locale of data stored in /data subpages with some modifications to module pages or 2. keep locale of data stored in /data subpages with some modifications to module pages and another module that deals with localization.
  • Module should be able to account for different locales of our sister wikis. This is primarily done through M:I18n.
  • Thrown errors should be in the wiki's locale.
  • See Dev_Wiki:Internationalization for more details on the project.

Contributions

  • All edits to modules and submodules will assume that they were made in good faith. As long as contributed code does not break existing functionality, we have loose enforcement on who and how contributions are made.
    • If you follow the development standards above then your code should be of high quality to be used on our wiki and in our sister wikis.
    • Please settle disputes or issues in a respectful and friendly demeanor. If needed, an Administrator or Moderator can arbitrate.
  • Expect your code to be edited or viewed by other contributors. All changes are made public through Special:RecentChanges and Special:RecentChangesLinked. As such, no one truly has individual ownership of code contributed on the wiki and as a community we make this wiki better, one line at a time.
  • Major refactorings of working code should be done in the separate /dev subpage or user page. This is so that articles that use these modules will still be readable by other people. We do not want to "push broken code to production".
  • If fixes cannot be made in a timely manner, copy/paste broken code into a separate /dev subpage or user page and revert back changes to the recent stable release.
  • Every contributor is expected to follow or understand the above guidelines. If you have any questions or concerns please feel free to contact the wiki's Administrators or Moderators.

References

Advertisement