WARFRAME Wiki
Advertisement
WARFRAME Wiki


Weapons contains all of WARFRAME's non-modular weapon data.

Usage

Template

In template and articles: {{#invoke:Weapons|function|input1|input2|...}}

Navigation

Quick navigation to submodules:

Product Backlog

Name Type Status Priority Assignee Description Date Issued Last Update
Module:StatObject as OOP paradigm Dev Planning Low

Currently our usage of Module:StatObject is as a static class with statRead and statFormat functions. Update Module:Weapons and Module:StatObject so we can 'instantiate' an actual StatObject object that takes in a weapon table entry as an argument. This way we can just do:

local StatObject = require('Module:StatObject') -- Base class
local WeaponData = require('Module:Weapons/data')

-- Doing some metaprogramming to extend functionality of StatObject class
StatObject.default = {
    Name = { nil, 'Weapon Name: %s' },  -- Sample definition for Name field getter/formatter
    ...
}

local BratonStatObject = StatObject(WeaponData['Braton'])

-- Get raw Name value "Braton" instead of StatObject.statRead(WeaponData['Braton'], 'Name')
local name = BratonStatObject.Name
-- Get formatted Name value "Weapon Name: Braton" (as defined in StatObject.default) instead of StatObject.statFormat(WeaponData['Braton'], 'Name')
print(BratonStatObject.Name)
mw.log(BratonStatObject.Name)
local formattedName = tostring(BratonStatObject.Name)
-- If the above is not possible in Lua then maybe add a __call metamethod to Name key to return its formatted value
formattedName = BratonStatObject.Name()
-- Or add a format() function to instantiated StatObject's metatable, passing in key name as argument
formattedName = BratonStatObject:format('Name')
22:01, 5 December 2022 (UTC)
Include attack name/context in Module:Weapons/ppdata Dev Planning Low

Update Module:Weapons/ppdata/seeder to add attack names associated with the respective stats used for comparing so that Module:Weapons/characteristics can add additional context to the stat comparisons. See https://warframe.fandom.com/wiki/Quassus?commentId=4400000000003635575. For Quassus Quassus's case, JatKusar Jat Kusar has a base 35% crit chance, but since we are comparing against non-normal attacks, Quassus Quassus's Ethereal Daggers will have second highest crit chance (30%) behind TenetExec Tenet Exec's slam shockwaves (38%).

22:09, 5 August 2022 (UTC)
Weapon and Attack classes Refactor and Dev Planning Low
  • Create a new Weapon and Attack class that can be instantiated by passing in a weapon table entry and attack table entry respectively. Each of these classes should contain a statRead() and statFormat() function that can be called to return an particular weapon stat, aggregate data, or computed stat based on /data contents.
21:18, 18 January 2022 (UTC)
ExplosionDelay key Refactor Planning Low
  • Update this to store a table with two values: shortest delay time and longest delay time. This is to support Kompressa Kompressa's variable delay time. For weapons that have a single delay time, use the same value for both table elements.
22:10, 6 January 2022 (UTC)
Reload key Refactor Planning Low
  • Move Reload key to attack tables? Nagantaka and Ambassador have two different reload times depending on attack.
  • 21:35, 19 January 2022 (UTC) update: User:Gigamicro suggests that any duplicate keys nested in Attack table entries should 'override' the base values for the weapon. In this case, add a another Reload key under the appropriate attack that has a different value than the Reload key in the main weapon entry.
Augments in /data Refactor New Low
  • Remove weapon augments list from /data and use Module:Mods and/or Module:Mods/data instead to fetch augment mods data.
    • Would require dev work in M:Mods/data too to index by mod type.
01:37, 31 May 2021 (UTC)
Data validation Dev/database Active Medium

Create Module:Weapons/data/validate subpage of /data for data validation functions

  • Include type checking for each column/attribute
  • Include checking if a table entry has the required keys (the minimum number of keys needed to support basic features in Module:Weapons)
  • Include boundary checking for stat values (e.g. CritChance cannot be negative)
01:37, 31 May 2021 (UTC) 23:33, 1 August 2021 (UTC)
Error handling Clean up New Medium

Change all return statements with "ERROR" to either assert() or error() to standardize error handling.

  • Error messages should be in the form of "functionName(argument names): argument value 1 is not a valid number".
01:37, 31 May 2021 (UTC)
Update database schema Database Active Medium User:Cephalon Scientia

Reworking how attacks are stored in tables for flexibility. Should have one Attack column that contains multiple tables, each representing a unique attack for that weapon. Would probably improve/simplify Weapon Comparison and Template:WeaponInfoboxAutomatic in displaying multiple attacks of a weapon. Right now we are hacking the use of ChargeAttack and SecondaryAttack for some attacks that are not necessarily charged or alt-fire (see Deconstructor's entry in the database). Also include forced proc data for all possible attacks (e.g. Glaives, some forced Impact weapons, etc.).

23:33, 1 August 2021 (UTC) update: There are lots of changes to these tables as I slowly create validation functions to check what keys-value pairs are needed or not, see documentation in Module:Weapons/data/doc for possible key-value pairs. Right now, attacks are stored in generic Attack1, Attack2, ... keys when we were changing the names of attack keys (e.g. NormalAttack became Attack1).

21:18, 18 January 2022 (UTC) update: User:Gigamicro implemented a new attack key (should be named Attacks to match key naming convention) that points to an array of attack tables. Attack1 to Attack9 keys are still in the data, just we now have a new way of indexing the same attack data by reference.

21:35, 19 January 2022 (UTC) update: Attack1 to Attack9 are now depreciated and removed from data tables. All optional keys are now explicitly stored in database (but are still optional b/c we set default values for getter functions in Module:Weapons. Some error clean up is still needed but for the most part, all weapon submodules and weapon tooltips should properly use Attacks table.

01:50, 31 May 2021 (UTC) 21:35, 19 January 2022 (UTC)
Unit tests Testing Archived High User:Cephalon Scientia

Add unit tests in Module:Weapons/testcases for each function in Module:Weapons. See Module:Math/testcases for examples and https://dev.fandom.com/wiki/Global_Lua_Modules/Testharness for documentation on how to format tests.

20:29, 31 July 2021 (UTC) update: Do not feel like it is appropriate to add unit tests using Module:TestHarness to most of the functions in this module since they mostly pertain to building wikitext to display to the reader. We can add a Module:Weapons/testcases subpage for visual tests to ensure rendered wikitext is not broken. Otherwise, I think it is more important to validate the data in Module:Weapons/data which are being formatted and displayed to the reader.

02:01, 31 May 2021 (UTC) 20:29, 31 July 2021 (UTC)

Finished Issues

Name Type Status Priority Assignee Description Date Issued Completion Date
Advantages/disadvantages Refactor Completed Low User:Cephalon Scientia
  • Move advantages/disadvantages builder to a new submodule page for organization.
06:05, 3 October 2021 (UTC) 17:47, 2 November 2021 (UTC)
Railjack Weapons Dev/Edit/Database Long-term support Medium User:Cephalon Scientia
  • Officially support Railjack weapons being in database. This allows Railjack weapons to make use of our infobox builder and do automatic comparisons.
  • Update /data with current user-contributed data in Railjack/Armaments

02:32, 6 September 2021 (UTC) update: Added most Railjack turrets and ordnances to /data. Missing Zetki Photor MK II, Zetki Carcinnox MK II, and Zetki Apoc MK I. First two are not obtainable in-game at this time since they are missing from drop tables, but are still in players' inventories.

00:12, 29 September 2021 (UTC) update: Added Zetki Photor MK II, Zetki Carcinnox MK II, and Zetki Apoc MK I stats according to the Mobile Export.

22:17, 2 August 2021 (UTC) 00:12, 29 September 2021 (UTC)
Update Conclave database schema Database Long-term support Low User:Cephalon Scientia

Remove keys that represent PvE stats as they are irrelevant to PvP. Most other key-value pairs (except those in attack tables) are shared with /data and can be looped through to add shared key-value pairs.

06:18, 10 August 2021 (UTC) 03:10, 16 August 2021 (UTC)
Clean up Clean up Completed Medium User:Cephalon Scientia
  • Remove unused functions. Also remove redundant functions that can be otherwise be used as as simple condition check (e.g. HasAttack() and DoNotHasAttack()).
  • Standardize styling to WARFRAME Wiki:Programming Standards.
01:37, 31 May 2021 (UTC) 06:20, 10 August 2021 (UTC)
Refactoring Refactor Long-term support Medium User:Cephalon Scientia

Refactor these functionalities for code reuse, better performance, better maintainability, and etc.:

  • Weapon comparison tables (see Weapon Comparison) ✔️
    • Includes Conclave ✔️
    • 00:19, 7 August 2021 (UTC) update: Weapon comparison tables are moved to Module:Weapons/comptable submodule for seperations of concerns design
  • Polarity table (see Polarity) ✔️
  • Mastery table (see Mastery Rank) ✔️
  • Highest physical damage type table (see Damage/Impact Damage) ✔️
  • Getter function(s) for weapon statistics/properties ✔️
  • Weapon gallery ✔️
17:43, 3 June 2021 (UTC) 00:19, 7 August 2021 (UTC)
Documentation Documentation New High

Add LuaDoc-style documentation for all functions.

01:37, 31 May 2021 (UTC) 06:35, 31 July 2021 (UTC)
Comparison tables and comparing two weapons Refactor Completed High User:Cephalon Scientia

Refactor BuildCompRow(), BuildCompTable(), BuildGunComparisonString, and their sister functions.

  • BuildCompRow() should have at max 2 nested if/else blocks
  • Do not remove functions such as getCompTableGuns(frame) and p.getCompTableArchGuns(frame) as those will be exposed to articles. Do not think there is a way to pass in table arguments in {{#invoke:}} calls so we could not have a single table builder function. Not true, can pass a multitude of named arguments and use frame.args['argName'] to get those arguments.

20:53, 29 July 2021 (UTC) update: We now use getValue(Weapon, keyName, attackName) and dictionaries (e.g. GUN_KEY_MAP) that contain simple getter functions to get specific weapon stat values. No more complicated nested if/else blocks.

01:37, 31 May 2021 (UTC) 20:53, 29 July 2021 (UTC)
getAttackValue() Refactor Completed Medium User:Cephalon Scientia

Refactor getAttackValue(Weapon, Attack, ValName, giveDefault, asString, forTable).

  • If/else blocks can be converted into a dictionary or objects that can be constructed through the use of tables and metatables.

20:53, 29 July 2021 (UTC) update: getAttackValue() has been removed and its functionality is moved to getValue(Weapon, keyName, attackName) using dictionaries (e.g. GUN_KEY_MAP) that contain simple getter functions.

01:37, 31 May 2021 (UTC) 20:53, 29 July 2021 (UTC)
p.getRivenDispositionTable() Refactor Completed Low
  • Optimized p.getRivenDispositionTable() so we don't literally have to perform over a thousand loops through each exact possible Disposition value to find weapons with that exact Disposition.
  • Remove p.getRivenDispositionList() since it is not invoked by itself on pages and was used by old p.getRivenDispositionTable().
3:42, 21 July 2021 (UTC) 4:49, 21 July 2021 (UTC)
p.buildDamageTypeTable(frame) Refactor Completed Low
  • Use multi-line strings for table building.
  • Updated validate function to only get non-Kitgun weapons and only display damage type distribution of Attack1 or the attack listed in TooltipAttackDisplay.
  • Refactored to modern programming standards.
01:37, 31 May 2021 (UTC) 21:54, 20 July 2021 (UTC)
p.buildAutoboxCategories(frame) Refactor Completed Medium

Implement a map/dictionary for mapping traits and trigger types to category link.

01:37, 31 May 2021 (UTC) 21:54, 20 July 2021 (UTC)
Weapon nav Dev Completed Medium User:FINNER

Add a new function that constructs the same navbox as Template:WeaponNav. Goal is to reduce memory used by calling Template:Weapon 400+ times on every weapon page as well as automating navbox updates whenever a new weapon is added. Right now, Template:Weapon uses ~11MB, sometimes ~20MB on pages like Volnus Prime.

Weapon navigation box generator resides in Module:Weapons/nav.

03:24, 5 June 2021 (UTC) 6:07, 7 June 2021 (UTC)
Weapon infobox Dev Completed Medium User:FINNER

Migrate wikitext from Template:WeaponInfoboxAutomatic into a infobox builder function.

  • This will probably remove the need for getValue() and their equilvalents.

Weapon infobox generator resides in Module:Weapons/infobox.

01:37, 31 May 2021 (UTC) 22:14, 8 June 2021 (UTC)
High lua memory usage Dev/Debugging Completed High User:FINNER

Some weapon pages have unusually high memory usage for Lua scripts, this will be problematic the moment we add new weapons to Template:WeaponNav:

Normally, memory usage is ~37 MB which is why this is odd.

This issue has been fixed when we now generate Template:WeaponNav using this module, instead of calling Template:Weapon 400+ times per page.

02:04, 1 June 2021 (UTC) 18:03, 7 June 2021 (UTC)

Forked Repos

Documentation

Package items

weapons._isVariant(weaponName) (function)
Checks if a weapon is a variant or not.
Parameter: weaponName Weapon name (string)
Returns:
  • True if weapon is a variant, false otherwise (boolean)
  • Weapon's variant name or "Base" if weapon is not a variant (string)
  • Weapon name, same as weaponName (string)
weapons._buildName(baseName, variant) (function)
Builds the full name of a weapon's variant. Does not check if it exists or not.
Parameters:
  • baseName Weapon's base name (e.g. "Braton") (string)
  • variant Variant name (e.g. "Vandal"); if nil, returns base weapon name instead (string; optional)
Returns: Weapon's variant name (e.g. "Braton Vandal") (string)
weapons._getWeapon(weaponName, getConclave) (function)
Returns a specific weapon table entry from /data or /Conclave/data.
Parameters:
  • weaponName Weapon name (string)
  • getConclave If true, gets PvP stats of weapon instead, false otherwise; defaults to false (boolean; optional)
Returns: Weapon table (table)
weapons._getValue(Weapon, key, attack) (function)
Gets the raw value of a certain statistic of a weapon.
Parameters:
  • Weapon Weapon table (table)
  • key Name of key (string)
  • attack Name of attack to search through; defaults to 'Attack1' or what '_TooltipAttackDisplay' is set to (string; optional)
Returns: Value of statistic (string, number)
weapons._getFormattedValue(Weapon, keyName, attackName) (function)
Gets the formatted value of a certain statistic of a weapon to be displayed the wiki.
Parameters:
  • Weapon Weapon table (table)
  • keyName Name of key (string)
  • attackName Name of attack to search through; defaults to 'Attack1' (string; optional)
Returns: Value of statistic (string)
weapons._attackLoop(Weapon) (function)
Loops through all possible attacks that a weapon may have. use ipairs(Weapon.attack) instead
Parameter: Weapon Weapon entry as seen in /data (table)
Returns:
  • An iterator function that returns the key-value pair of next attack entry (function)
  • Original weapon entry passed into function (table)
weapons._getWeapons(validateFunction, getConclave, ignoreIgnore) (function)
Returns a subset of /data or /Conclave/data based on a validation function.
Parameters:
  • validateFunction Function that filters out a weapon by taking in a Weapon table argument (function)
  • getConclave If true, gets PvP stats of weapons instead, false otherwise; defaults to false (boolean)
  • ignoreIgnore If true, ignores the _IgnoreEntry flag, false otherwise; defaults to false (boolean)
Returns: Table of weapon table entries as seen in /data (table)
weapons._getMeleeWeapons(weapClass, pvp) (function)
Returns all melee weapons. If weapType is not nil, only grab for a specific type For example, if weapType is "Nikana", only pull Nikanas.
Parameters:
  • weapClass Name of melee class to filter by; if nil then gets all melee weapons (string; optional)
  • pvp If true, only gets melee weapons available in Conclave, false otherwise; defaults to false (boolean; optional)
Returns: An array of melee weapon table entries as seen in /data (table)
weapons._getConclaveMeleeWeapons(weapClass) (function)
Returns all conclave melee weapons. If weapType is not nil, only grab for a specific type For example, if weapType is "Nikana", only pull Nikanas.
Parameter: weapClass Name of melee class to filter by; if nil then gets all melee weapons (string; optional)
Returns: An array of melee weapon table entries as seen in /Conclave/data (table)
weapons.getStanceWeaponList(table}) (function)
Builds list of weapons that can equip a particlar stance mod as seen on Template:StanceWeapons.
Parameter: table} frame Frame object w/ first argument being string stanceName
Returns: Resultant wikitext of comparison list (string)
weapons.getMeleeWeaponGallery(frame) (function)
Builds a melee weapon gallery as seen on Template:MeleeCategory.
Parameter: frame Frame object w/ first argumenting being string meleeClass (table)
Returns: Resultant wikitext of gallery (string)
weapons.getWeaponCount(frame) (function)
Gets the total count of weapons as used on Mastery Rank#Total Mastery.
Parameter: frame Frame object w/ the first argument being the weaponSlot and the second argument being a boolean to getFullList (table)
Returns: Total count of weapons in a certain category/type (number)
weapons._getCategory(weapon) (function)
Gets the weapon class of weapons for use in comparison tables.
Parameter: weapon Weapon table (table)
Returns: Category name, {string} Triggertype (string)
weapons.getPolarityTable(frame) (function)
Builds wikitable of all weapons' innate polarities as seen on Polarity.
Parameter: frame Frame object (table)
Returns: Wikitext of resultant wikitable (string)
weapons.buildComparison(frame) (function)
Builds comparison list between two weapons in PvE.
Parameter: frame Frame object (table)
Returns: Resultant wikitext of comparison list (string)
weapons.buildComparison(frame) (function)
Builds comparison list between two weapons in PvP (Conclave).
Parameter: frame Frame object (table)
Returns: Resultant wikitext of comparison list (string)
weapons.buildDamageTypeTable(frame) (function)
Builds a table that lists out all weapons with a certain damage type
Parameter: frame Frame object (table)
Returns: Wikitext of resultant wikitable (string)
weapons._shortLinkList(Weapon, tooltip) (function)
Builds a list of weapons, with variants being next to base weapon name inside parentheses (e. g. Braton Braton (Braton MK1, BratonPrime Prime)).
Parameters:
  • Weapon Weapon table (table)
  • tooltip If true, adds weapon tooltips, false otherwise; defaults to false (boolean)
Returns: Wikitext of resultant list (string)
weapons.getMasteryShortList(frame) (function)
Builds a list of weapons' mastery requirements as seen on Template:EquipmentUnlock, Template:EquipmentUnlock/Primary, Template:EquipmentUnlock/Secondary, Template:EquipmentUnlock/Melee, etc.
Parameter: frame Frame object w/ first argument being a string weaponSlot (table)
Returns: Wikitext of resultant list (string)
weapons.getConclaveList(frame) (function)
Builds a list of PvP weapons as seen on PvP#Limitations.
Parameter: frame Frame object w/ first argument being a string weaponSlot (table)
Returns: Wikitext of resultant list (string)
weapons.getRivenDispositionTable(frame) (function)
Builds a disposition wikitable as seen on Riven Mods/Weapon Dispos.
Parameter: frame Frame object w/ first argument being a string weaponSlot (table)
Returns: Wikitext of resultant wikitable (string)

Other items

getWeaponGallery(Weapons) (function)
Builds a weapon gallery.
Parameter: Weapons Array of weapon table entries to be displayed (table)
Returns: Resultant wikitext of gallery local function getWeaponGallery(Weapons) local result = {'') return table.concat(result, '\n') end local result = {} -- local result = { '{| style="margin:auto;text-align:center;"' } -- local nameRow = {} for i, Weapon in ipairs(Weapons) do -- table.insert(result, ('|style="width:165px"|File:%s'):format(statRead(Weapon, nil,'Image'),statRead(Weapon, nil,'Link'))) -- table.insert(nameRow, ('| style="vertical-align: text-top;" |%s'):format(statFormat(Weapon, nil,'NameLink'))) table.insert(result, statFormat(Weapon, nil,'NameLink')) -- if i % 5 == 0 then -- table.insert(result, table.concat(nameRow,'\n')) -- table.insert(result, "|-") -- nameRow = {} -- end end -- table.insert(result, table.concat(nameRow,'\n')) -- table.insert(result, '|}') return table.concat(result, '\n') end (string)

Created with Docbunto

See Also

Code


---	'''Weapons''' contains all of [[WARFRAME]]'s non-modular [[Weapons|weapon]] data.<br />
--	
--	@module			 weapons
--	@alias			p
--	@author			[[User:Falterfire|Falterfire]]
--	@attribution	[[User:Gigamicro|Gigamicro]]
--	@attribution	[[User:Flaicher|Flaicher]]
--	@attribution	[[User:FINNER|FINNER]]
--	@attribution	[[User:Cephalon Scientia|Cephalon Scientia]]
--	@attribution	[[User:Synthtech|Synthtech]]
--	@attribution	[[User:Calenhed|Calenhed]]
--	@image		IconPrimaryWeaponRifle.png
--	@require	[[Module:Table]]
--	@require	[[Module:Mods/data]]
--	@require	[[Module:Modular/data]]
--	@require	[[Module:Stances/data]]
--	@require	[[Module:Tooltips]]
--	@require	[[Module:Version]]
--	@require	[[Module:Weapons/data]]
--	@require	[[Module:Weapons/Conclave/data]]
--	@release	unstable
--	<nowiki>

-- TODO: Add LuaDoc style comments to new functions
-- TODO: Migrate comparison string builders to a submodule
local p = {}

local ConclaveData = mw.loadData [[Module:Weapons/Conclave/data]]
local StanceData = mw.loadData [[Module:Stances/data]]
local WeaponData = mw.loadData [[Module:Weapons/data]]

local Tooltip = require [[Module:Tooltips]] -- getFullTooptip
local Version = require [[Module:Version]] -- _getVersion, _getVersionDate
local Table = require [[Module:Table]] -- size, skpairs
local Polarity = require [[Module:Polarity]] -- _pols, _polarity

-- TODO: Could use M:DamageTypes instead of this table?
-- TODO: Remove ProgenitorBonus and MinProgenitorBonus keys from /data and only store
-- base values. In-game progenitor bonuses are implemented as a separate upgrade that is applied
-- to the base stats. If doing it this way, we have to remove MinProgenitorBonus as a key from
-- M:DamageTypes/data and update comparison string tables accordingly. In addition, a disclaimer
-- to Kuva/Tenet weapons may be added to weapon infobox saying that this weapon has a 'randomized' damage bonus
local DAMAGE_TYPES = {
	"Impact", "Puncture", "Slash",
	"Heat", "Cold", "Toxin", "Electricity",
	"Blast", "Corrosive", "Radiation", "Magnetic", "Gas", "Viral",
	"Void", "True", "ProgenitorBonus"
}

local VARIANT_LIST = {
	"Prime", "Prisma", "Wraith", "Vandal", "Vaykor", "Synoid", 
	"Telos", "Secura", "Sancti", "Rakta", "Mara", "MK1", "Kuva"
}

table.unpack = table.unpack or unpack

local function dmgTooltip(dt, addText)
	local text = (addText) and dt or ''
	return ('<span class="tooltip" data-param="%s" data-param2="DamageTypes">[[File:Dmg%sSmall64.png|x19px|link=]]&nbsp;%s</span>')
		:format(dt, dt, text)
end

--declare here so scope includes metatable functions
local default

--use after calc functions that might be repeated (cache[weapon][attack][key])
local cache = {}
local function cacheIn(weap,atk,key,val)
	cache[weap] = cache[weap] or {Name=weap.Name}
	cache[weap][atk] = cache[weap][atk] or {}
	cache[weap][atk][key] = val or cache[weap][atk][key]
	return cache[weap][atk][key]
end
local function cacheOut(weap,atk,key)
	return ((cache[weap]or{})[atk]or{})[key]
end

local function get(weap,atk,k)
	-- local c = cacheOut(weap,atk,k)
	-- if c then return table.unpack(c) end
	return atk and atk[k] or weap[k]
	-- return atk and (atk[k] or atk.BaseAttack and get(weap,atk.BaseAttack,k)) or weap[k]
end

local function getAttack(weap,atk)
	if type(atk)=='table' then return atk end
	local key = atk or weap['_TooltipAttackDisplay'] or 'Attack1'
	if type(key)=='string' then key = key:match'[0-9.]+' + 0 end
	return weap.attack[key]
end
local function statRead(weap,atk,k)
	atk = getAttack(weap,atk)

	local c = cacheOut(weap,atk,k)
	if c then return table.unpack(c) end

	if type(default[k])~='table' then
		default[k] = {default[k]}
	end
	-- local map, format = table.unpack(default[k])
	local map = default[k][1]
	default[k][1] = type(map)=='function' and map or (not map and get) or function(w,a,k)return get(w,a,k) or map end
	-- default[k][2] = hasFormat(format) and format or makeFormat(default[k]) -- may already be taken care of by formatmt

	return default[k][1](weap,atk,k)
end

local function hasFormat(v)
	local a,b = pcall(function()return v.format end)
	return a and b
end
local function makeFormat(maps)
	local format = maps[2]
	if type(format)=='function' or not format then
		-- first param using : is the table being indexed, so that's going to be the map
		-- prob completely unnecessary tbh
		maps.format = format or function(self,...)return table.concat({...},' '):gsub('(%d%.%d%d)%d*','%1') end
		maps[2] = maps
		return maps
	elseif type(format)=='table' and (format[1] or format.sep) then
		function maps.format(self,...)
			local t = {}
			for i,v in pairs{...} do -- ipairs can't deal with skips
				local w=format[i]
				if type(w)=='function' then
					t[i] = w(format,v)
				elseif w=='' then -- omit
				elseif type(w)=='string' then
					t[i] = v and w:format(v) or w:gsub('%%%a','N/A')
				else
					t[i] = v
				end
			end
			return table.concat(t,format.sep or ' ')
		end
		maps[2] = maps
		return maps[2]
	end
	-- map[2] is not: a table with a 'format' key/a string, a function, nil/false, or an array
	-- this should never happen
	mw.log'hey uhh this map is invalid'
	mw.log(mw.dumpObject(map))
	mw.log(debug.traceback())
	error'Invalid map'
end
local function statFormat(weap,atk,k, ...)
	local value = {statRead(weap,atk,k)}
	local format = default[k][2]
	default[k][2] = hasFormat(format) and format or makeFormat(default[k])
	return default[k][2]:format(table.unpack(value))
end

-- metafunctions to return functions with certain parameters
-- maps[1]
local function gets(k)
	return function(weap, atk)
		return get(weap,atk,k)
	end
end
local function unpacks(var)
	return function(weap, atk)
		local v = get(weap,atk,var)
		return type(v)~='table' and v or table.unpack(v)
	end
end
local function ors(...)
	local t = {...}
	t = #t > 1 and t or t[1]
	return function(weap, atk, self)
		local val
		for _,v in pairs(t) do
			if false then
			elseif type(v)=='string' then val=get(weap,atk,v) or (self~=v and statRead(weap,atk,v)) or nil
			elseif type(v)=='function' then val=v(weap,atk,t)
			elseif type(v)=='boolean' then return v
			end
			if val then return val end
		end
		val = t[#t]
		return type(val)~='function' and val or nil
	end
end
local function indexes(var, index)
	return function(weap, atk, self)
		local v = (self == index) and { get(weap, atk, var) } or { statRead(weap, atk, var) }
		if (#v <= 1) then v = v[1] end
		if type(v)=='function' then error(mw.dumpObject{ var=var, index=index, weap=weap, atk=atk, self=self, v=v }) end
		-- v = #v>1 and v or v[1]
		return type(v)=='table' and v[index] or 
		error('indexes return function given '..mw.dumpObject(v)..', asked for ['..index..']')
	end
end

-- maps[2]
local function passes(func)
	return function(self, ...)
		return func(...)
	end
end
local function percent(s, v)
	return ('%.2f%%'):format(100*(v or s))
end
local function percents(s)
	return function(self, v, ...)
		return s:format(100*v, ...)
	end
end

default = {
	-- some setup so default[2][k] works
	setmetatable({ k = 1 }, formatmt),
	setmetatable({ k = 2 }, formatmt),

	-- arrays of default values and format strings
	key = {'default','%.2format'},
	-- can generate values/formatting with functions
	-- 'val,...' is the return from map[1]
	key = {function(weap,atk)end,function(self,val,...)end},
	-- If format is a table the return values will be passed to each function/format string in order (nil is a passthrough)
	key = {function(weap,atk)return a,b,c,d end,{function(self,val)end,'%s',nil,'%d',sep=''}},
	-- can omit second entry, can omit table
	key = 'default',
	-- nil means get from attack or weapon (same as omitting)
	key = nil,
	-- begin real keys --

	AttackName = 'Normal',
	-- AttackName = "Normal Attack", -- which is better default attack name?
	AmmoCost = 1,
	DamageBias = {function(weap, atk)
		if not atk.Damage then
			error'DamageBias: no Attack.Damage'
		end
		local total, bestdmg, bestdt = 0, 0, nil
		for dt, dmg in pairs(atk.Damage) do
			-- assume +60% progenitor bonus damage ('MinProgenitorBonus' is +25%)
			local dmg = dt=='MinProgenitorBonus' and dmg*2.4 or dmg
			if dmg > bestdmg then
				bestdmg, bestdt = dmg, dt
			end
			total = total + dmg
		end
		if bestdmg==0 then
			error('DamageBias: empty Attack.Damage; Attack is'..mw.dumpObject(atk))
		end
		return table.unpack(cacheIn(weap,atk,'DamageBias',{bestdmg/total, bestdt, total}))
	end,{percent,passes(dmgTooltip),''}},
	BiasPortion = {indexes('DamageBias',1), percent},
	BiasType = {indexes('DamageBias',2), passes(dmgTooltip)},
	BaseDamage = {indexes('DamageBias',3), '%d'},
	TotalDamage = function(weap, atk)
		return statRead(weap,atk,'BaseDamage') * statRead(weap,atk,'Multishot')
	end,
	ChargeTime = {0,'%.1fs'},
	ExtraHeadshotDmg = {0, percents'+%.2f%%'},
	Falloff = {function(weap, atk)
		local fo=get(weap,atk,'Falloff') or {}
		return fo.StartRange or 0, fo.EndRange or math.huge, fo.Reduction or 1
	end, {'%.1fm (100%%) -','%.1fm', percents'(%.2f%%)'}},
	-- Falloff = function(weap, atk) local fo=atk.Falloff or weap.Falloff return fo.StartRange, fo.EndRange, fo.Reduction end,
	FalloffStart = {indexes('Falloff',1),'%.1fm'},
	FalloffEnd = {indexes('Falloff',2),'%.1fm'},
	FalloffReduction = {indexes('Falloff',3),percent},
	HeadshotMultiplier = {1,'%.1fx'},
	Multishot = 1,
	PunchThrough = {0,'%.1fm'},
	ShotSpeed = {'N/A','%.1fm/s'},
	BurstDelay = {0,'%.1fs'},
	CritChance = {0,percent},
	CritMultiplier = {1,'%.1fx'},
	ForcedProcs = {unpacks'ForcedProcs',{sep=', '}},
	Radius = {0,'%.1fm'},
	StatusChance = {0,percent},
	Disposition = {function(w,a)
		local d = get(w,a,'Disposition')
		return d or 0/0, type(d)=='number' and math.floor(5*(d-(d<1 and 0.3 or 0.309))) or 0/0
	end, function(s,v,d)
		return default.Dispo[2](s,d)..(' (%.2fx)'):format(v)
	end},
	Dispo = {indexes('Disposition',2), function(s,d)
		return d and d==d and ('●'):rep(math.min(d,5))..('○'):rep(5-d) or '×××××'
	end},
	Introduced = function(weap)
		return weap['Introduced'] and Version._getVersion(weap['Introduced'])['Name'] or 'N/A'
	end,
	IntroducedDate = function(weap)
		return weap['Introduced'] and Version._getVersionDate(weap['Introduced']) or 'N/A'
	end,
	Mastery = 0,
	Link = {nil,'[[%s]]'},
	Name = {nil,function(s,v)return Tooltip.getFullTooltip(v,'Weapons') end},
	NameLink = {function(weap) return weap.Link,weap.Name end, '[[%s|%s]]'},
	Polarities = {nil, passes(Polarity._pols)},
	Traits = {unpacks'Traits',{sep=', '}},
	Accuracy = 0,
	--[=[AmmoType = function(weap)
		return weap['AmmoType'] or (weap['Slot']=='Secondary' and 'Pistol') or ({
			Rifle='Rifle',
			Shotgun='Shotgun',
			['Bow'] = 'Sniper',['Sniper Rifle'] = 'Sniper',['Launcher'] = 'Sniper',
		})[weap['Class']] or ''
	end,--]=]
	AmmoType = function(weap,atk)
		return get(weap,atk,'AmmoType') or ({
			['Arch-Gun (Atmosphere)']='Heavy',
			['Secondary']='Pistol',
		})[weap['Slot']] or ({
			['Rifle']='Rifle',
			['Shotgun']='Shotgun',
			['Bow']='Sniper',['Sniper Rifle']='Sniper',['Launcher']='Sniper',
		})[weap['Class']] or 'None'
	end,
	ExilusPolarity = {nil, passes(Polarity._polarity)},
	Magazine = 1,
	MaxAmmo = 0,
	Range = function(weap, atk) return get(weap,atk,'Range') or statRead(weap,atk,'ShotType') == 'Hit-Scan' and 300 or 0 end,
	Reload = ors('Reload','RechargeTime',0),
	RechargeTime = function(weap,atk)
		return statRead(weap,atk,'ReloadStyle'):find'[Rr]egen' and statRead(weap,atk,'Magazine') / statRead(weap,atk,'ReloadRate') or nil
	end,
	ReloadRate = 0,
	ReloadDelay = function(weap,atk)return get(weap,atk, ReloadDelay) or statRead(weap,atk,'Magazine')>1 and 0.2 or 0 end, -- approx
	ReloadDelayEmpty = ors('ReloadDelayEmpty', 'ReloadDelay'),
	ReloadStyle = 'Magazine',
	Spool = 0,
	Trigger = 'N/A',
	BlockAngle = {0, '%d&#176;'},
	ComboDur = 0,
	FollowThrough = 0,
	HeavyAttack = 0,
	HeavyRadialDmg = 0,
	HeavySlamRadius = 0,
	MeleeRange = 0,
	SlamAttack = 0,
	SlamRadialDmg = 0,
	SlamRadius = 0,
	SlideAttack = 0,
	Stances = getWeaponStanceList, -- ? can't find this function
	StancePolarity = {nil, passes(Polarity._polarity)},
	WindUp = 0,
	BurstCount = 1,
	AvgProcCount = function(weap, atk)
		return ( statRead(weap,atk,'StatusChance') + Table.size(statRead(weap,atk,'ForcedProcs')) ) * statRead(weap,atk,'Multishot')
	end,
	AvgProcPerSec = function(weap, atk)
		return statRead(weap, atk, 'AvgProcCount') * statRead(weap, atk, 'EffectiveFireRate')-- * statRead(weap, atk, 'Multishot') 
	end,
	InterShotTime = function(weap, atk)
		local v = statRead(weap,atk,'Magazine')==1 and statRead(weap,atk,'Reload') or 0
		if v==0 then v = 1/statRead(weap,atk,'FireRate') end
		return v
	end,
	EffectiveFireRate = function(weap, atk)
		return 1/( statRead(weap,atk,'ChargeTime')+statRead(weap,atk,'InterShotTime') )
	end,
	MagShots = function(weap,atk)
		return math.floor(statRead(weap, atk, 'Magazine') / statRead(weap, atk, 'AmmoCost'))
	end,
	FireRate = function(weap,atk)
		local dataFireRate = get(weap,atk,'FireRate')
		if dataFireRate then return dataFireRate end
		local count = statRead(weap, atk, 'BurstCount')
		local fireRate = count / (1/statRead(weap, atk, 'BurstFireRate') + count*statRead(weap, atk, 'BurstDelay'))
		-- if fireRate == fireRate and dataFireRate == dataFireRate and fireRate ~= dataFireRate then
		--	 mw.log('Fire rate of '..statRead(weap,atk,'Name')..': '..fireRate..' or '..dataFireRate..'?')
		--	 return dataFireRate
		-- else
		return fireRate
		-- end
	end,
	BurstFireRate = function(weap,atk)
		local dataBurstRate = get(weap,atk,'BurstFireRate')
		if dataBurstRate then return dataBurstRate end
		local count = statRead(weap, atk, 'BurstCount')
		local fireRate = statRead(weap, atk, 'FireRate')
		local burstRate = fireRate/(count - fireRate*count*statRead(weap, atk, 'BurstDelay'))
		-- if burstRate == burstRate and dataBurstRate == dataBurstRate and burstRate ~= dataBurstRate then
		--	 mw.log('Burst Fire rate of '..statRead(weap,atk,'Name')..': '..burstRate..' or '..dataBurstRate..'?')
		--	 return dataBurstRate
		-- else
		return burstRate
		-- end
	end,

	CalcDamage = function(weapon, attack)
		-- Count
		local burstCount = statRead(weapon, attack, 'BurstCount')
		local tapShots = burstCount
		local magTaps = statRead(weapon,attack,'MagShots')
		local multishot = statRead(weapon, attack, 'Multishot')

		-- Time
		local shotTime = statRead(weapon,attack,'InterShotTime')
		local chargeTime = statRead(weapon,attack,'ChargeTime')
		local tapTime = chargeTime + (burstCount-1) * statRead(weapon, attack, 'BurstDelay')
		local magDur = magTaps * tapTime
		if magDur == 0 then if shotTime == 0 then shotTime = 1/statRead(weapon,attack,'FireRate') end magDur=shotTime end
		local shotDelay = math.max(0,shotTime - tapTime)

		-- Damage
		local biasPortion, biasType, hitDamage = statRead(weapon, attack, 'DamageBias')
		local avgCritMult = 1 + (statRead(weapon, attack, 'CritMultiplier')-1)*statRead(weapon, attack, 'CritChance')
		local avgTap = hitDamage * avgCritMult * multishot * tapShots
		local avgMag = avgTap * magTaps
		local avgLifetimeDmg = avgMag * (1 + (statRead(weapon, attack, 'MaxAmmo')/statRead(weapon, attack, 'Magazine')))

		-- Damage / Time
		-- local avgShort = avgShot * multishot / shotTime
		-- local avgShort = avgMag / magDur
		local avgShort = avgTap / (tapTime + shotDelay)
		-- which calc is better tho?
		local avgSustained = avgMag/(magDur+statRead(weapon, attack, 'ReloadDelay')+statRead(weapon, attack, 'Reload'))

		--[=[
		mw.log(mw.dumpObject{statRead(weapon,attack,'Name'),
		})--]=]

		return table.unpack(cacheIn(weapon,attack,'CalcDamage',{hitDamage, avgTap, avgShort, avgSustained, avgLifetimeDmg, hitDamage * multishot / shotTime, avgMag}))
	end,
	ShotDmg = indexes('CalcDamage',1),
	AvgShotDmg = indexes('CalcDamage',2), AvgTapDmg = indexes('CalcDamage',2),
	BurstDps = indexes('CalcDamage',3),
	SustainedDps = indexes('CalcDamage',4),
	LifetimeDmg = indexes('CalcDamage',5),
	BaseDps = indexes('CalcDamage',6),
	MagDmg = indexes('CalcDamage',7),
	IsMelee = function(weap,atk)return statRead(weap,atk,'Slot'):find('Melee') and true or false end,
	IsSilent = ors('IsSilent','IsMelee', false),
	Conclave = false,
	Image = {'Panel.png','[[File:%s|link=]]'},
	attack = ors('attack', getAttack, {}),
	-- Family=nil,
	BaseName = function(weap,atk) return get(weap,atk,'BaseName') or ({p._getVariant(statRead(weap,atk,'Name'))})[3] end,
	-- TODO: Add comments to Explosion function for readability
	Explosion = function(weap,atk)
		-- tbh this is a mess
		local exp = get(weap,atk,'Explosion') or statRead(weap,atk,'AttackName'):gsub(' Impact','')..' Explosion'
		if type(exp)=='string' then
			exp = weap.attack[tonumber(exp:gsub('%D',''))] or exp
		elseif type(exp)=='number' then
			exp = weap.attack[exp] or exp
		end
		local explosions = {}
		-- if type(exp)=='table' then goto ret end
		if type(exp)~='table' then
		for i,v in ipairs(weap.attack) do
			if statRead(weap,v,'AttackName'):find 'xplosion' then
				if statRead(weap,v,'AttackName') == exp then
					explosions[1]=nil
					exp=v
					break
					-- goto ret
				end
				table.insert(explosions,v)
			end
		end
		exp = explosions[1] or exp
		end
		-- ::ret::
		cacheIn(weap,exp,'BaseAttack',{atk})
		return table.unpack(cacheIn(weap,atk,'Explosion',{exp}))
	end,
}
--loops for defaults table
for _, dt in ipairs(DAMAGE_TYPES) do
	default[dt] = function(weap, atk) return atk['Damage'][dt] or 0 end
end
default.MinProgenitorBonus = function(w,a) return a.Damage.MinProgenitorBonus or 0 end
default.ProgenitorBonus = function(w,a) return(a.Damage.MinProgenitorBonus or 0) * 2.4 end
for i = 1, 9 do
	default['Attack'..i] = function(weap, atk) return weap.attack[i] end
end

-- For mapping weapon traits or types to a category link
-- unused?
local CATEGORY_MAP = {
	Primary = 'Primary Weapons',
	Secondary = 'Secondary Weapons',
	Melee = 'Melee Weapons',
	['Arch-Melee'] = 'Archwing Melee',
	['Arch-Gun'] = 'Archwing Gun',
	['Arch-Gun (Atmosphere)'] = 'Archwing Gun',
	-- Kitgun = 'Kitgun',
	-- Zaw = 'Zaw',
	['Railjack Turret'] = 'Railjack',
	['Railjack Armament'] = 'Railjack',
	-- Gear = 'Gear',
	
	Rifle = 'Assault Rifle',
	-- ['Sniper Rifle'] = 'Sniper Rifle',
	-- Shotgun = 'Shotgun',
	-- Pistol = 'Pistol',
	-- ['Dual Pistols'] = 'Dual Pistols',
	-- Bow = 'Bow',
	-- Launcher = 'Launcher',
	-- ['Arm-Cannon'] = 'Arm-Cannon',
	-- ['Speargun'] = 'Speargun',
	-- Thrown = 'Thrown',
	-- ['Shotgun Sidearm'] = 'Shotgun Sidearm',
	
	Prime = 'Prime Weapons',
	-- ['Never Vaulted'] = 'Never Vaulted',
	-- Vaulted = 'Vaulted',
	-- Wraith = 'Wraith',
	-- Vandal = 'Vandal',
	-- Maybe replace with 'Kuva' category? Though technically Broken Scepter is a "Kuva" weapon which
	-- is why this distinction is made (in the past, editors mislabeled this trait)
	-- ['Kuva Lich'] = 'Kuva Lich',
	-- Prisma = 'Prisma',
	
	Grineer = 'Grineer Weapons',
	Corpus = 'Corpus Weapons',
	Infested = 'Infested Weapons',
	Tenno = 'Tenno Weapons',
	Sentient = 'Sentient Weapons',
	Entrati = 'Entrati Weapons',
	Baro = 'Baro Ki\'Teer Offering',
	Syndicate = 'Syndicate Offerings',
	-- ['Invasion Reward'] = 'Invasion Reward',
	
	['Alt Fire'] = 'Weapons with Alt Fire',
	['AoE'] = 'Weapons with Area of Effect]][[Category:Self Interrupt Weapons',
	
	-- Active = 'Active',
	Auto = 'Automatic',
	['Auto-Spool'] = 'Automatic',
	Burst = 'Burst Fire',
	['Auto-Burst'] = 'Burst Fire',
	['Auto Charge'] = 'Charge',
	-- Charge = 'Charge',
	Duplex = 'Duplex Fire',
	['Semi-Auto'] = 'Semi-Automatic',
	Held = 'Continuous Weapons'
}

---	Checks if a weapon is a variant or not.
--	@function		p._isVariant
--	@param			{string} weaponName Weapon name
--	@returns		{boolean} True if weapon is a variant, false otherwise
--	@returns		{string} Weapon's variant name or "Base" if weapon is not a variant
--	@returns		{string} Weapon name, same as weaponName
function p._isVariant(weaponName)
	for i, var in pairs(VARIANT_LIST) do
		if string.find(weaponName, var) then
			return true, var, (string.gsub(weaponName, " ?"..var.." ?-?", ""))
		end
	end
	return false, "Base", weaponName
end

---	Builds the full name of a weapon's variant. Does not check if it exists or not.
--	@function		p._buildName
--	@param			{string} baseName Weapon's base name (e.g. "Braton")
--	@param[opt]		{string} variant Variant name (e.g. "Vandal"); if nil, returns base weapon name instead
--	@returns		{string} Weapon's variant name (e.g. "Braton Vandal")
function p._buildName(baseName, variant)
	if not variant or variant == 'Base' or variant == '' then
		return baseName
	end
	return (({
		Prime= baseName~='Laser Rifle' and '%b %v',
		Vandal='%b %v',
		Wraith='%b %v',
		MK1='%v-%b',
	})[variant] or '%v %b'):gsub('%%v',variant):gsub('%%b',baseName)
end

---	Returns a specific weapon table entry from <code>/data</code> or <code>/Conclave/data</code>.
--	@function		p._getWeapon
--	@param			{string} weaponName Weapon name
--	@param[opt]		{boolean} getConclave If true, gets PvP stats of weapon instead, false otherwise; defaults to false
--	@returns		{table} Weapon table
function p._getWeapon(weaponName, getConclave)
	return (getConclave and ConclaveData or WeaponData)["Weapons"][weaponName] or
	error('p._getWeapon(weaponName, getConclave): "'..weaponName..
		'" does not exist in [[Module:Weapons/data]] or [[Module:Weapons/Conclave/data]]')
end

---	Gets the raw value of a certain statistic of a weapon.
--	@function		p._getValue
--	@param			{table} Weapon Weapon table
--	@param			{string} key Name of key
--	@param[opt]		{string} attack Name of attack to search through; defaults to 'Attack1' or what '_TooltipAttackDisplay' is set to
--	@returns		{string, number} Value of statistic
function p._getValue(weap, key, atk)--, formatted)
	-- return (formatted and statFormat or statRead)(weap,atk,key)
	return statRead(weap, atk, key)
end

---	Gets the formatted value of a certain statistic of a weapon to be displayed
--	the wiki.
--	@function		p._getFormattedValue
--	@param			{table} Weapon Weapon table
--	@param			{string} keyName Name of key
--	@param[opt]		{string} attackName Name of attack to search through; defaults to 'Attack1'
--	@returns		{string} Value of statistic
function p._getFormattedValue(weap, key, atk)
	-- return p._getValue(Weapon, keyName, attackName, true)
	return statFormat(weap, atk, key)
end

---	Loops through all possible attacks that a weapon may have.
--	use ipairs(Weapon.attack) instead
--	@function		p._attackLoop
--	@param			{table} Weapon Weapon entry as seen in <code>/data</code>
--	@returns		{function} An iterator function that returns the key-value pair of next attack entry
--	@returns		{table} Original weapon entry passed into function
function p._attackLoop(Weapon)
	if not Weapon then
		return function() return nil end
	end
	local function nextAttack(t, k)
		if not k then return '1', t['Damage'] and t or t['Attack1'] end
		local v
		repeat
			k, v = next(t,k)
		until type(v) == 'table' and v['Damage']
		return k, v
	end
	return nextAttack, Weapon
end

---	Returns a subset of <code>/data</code> or <code>/Conclave/data</code> based on a validation function.
--	@function		p._getWeapons
--	@param			{function} validateFunction Function that filters out a weapon by taking in a Weapon table argument
--	@param			{boolean} getConclave If true, gets PvP stats of weapons instead, false otherwise; defaults to false
--	@param			{boolean} ignoreIgnore If true, ignores the _IgnoreEntry flag, false otherwise; defaults to false
--	@returns		{table} Table of weapon table entries as seen in <code>/data</code>
function p._getWeapons(validateFunction, getConclave, ignoreIgnore, sortFunc)
	local weaps = {}
	for _, weap in pairs((getConclave and ConclaveData or WeaponData)["Weapons"]) do
		if (ignoreIgnore or not weap['_IgnoreEntry']) and validateFunction(weap) then
			table.insert(weaps, weap)
		end
	end
	table.sort(weaps,sortFunc or function(a,b)return a.Name<b.Name end)
	return weaps
end

---	Returns all melee weapons. If weapType is not nil, only grab for a specific type
--	For example, if weapType is "Nikana", only pull Nikanas.
--	@function		p._getMeleeWeapons
--	@param[opt]		{string} weapClass Name of melee class to filter by; if nil then gets all melee weapons
--	@param[opt]		{boolean} pvp If true, only gets melee weapons available in Conclave, false otherwise; defaults to false
--	@returns		{table} An array of melee weapon table entries as seen in <code>/data</code>
function p._getMeleeWeapons(weapClass, pvp)
	return p._getWeapons(function(weap)
		return weap.Slot == "Melee" and weapClass == weap.Class
	end, pvp)
end

---	Returns all conclave melee weapons. If weapType is not nil, only grab for a specific type
--	For example, if weapType is "Nikana", only pull Nikanas.
--	@function		p._getConclaveMeleeWeapons
--	@param[opt]		{string} weapClass Name of melee class to filter by; if nil then gets all melee weapons
--	@returns		{table} An array of melee weapon table entries as seen in <code>/Conclave/data</code>
function p._getConclaveMeleeWeapons(weapClass)
	return p._getMeleeWeapons(weapClass, true)
end

-- TODO: Move to M:Stances?
---	Gets stance mods for a particular melee class.
--	@function		getStances
--	@param			{string} meleeClass Melee class
--	@param			{boolean} ConclaveOnly If true, only gets PvP-exclusive stance mods, otherwise gets all applicable stance mods; default false
--	@returns		{table} Table of stance table entries as seen in <code>M:Stance/data</code>
local function getStances(meleeClass, ConclaveOnly)
	local stanceTable = {}
	for stanceName, Stance in pairs(StanceData) do
		if (not meleeClass or meleeClass == Stance.WeaponType)
			and (not ConclaveOnly)==(not Stance.ConclaveOnly) then
			stanceTable[stanceName] = Stance
		end
	end
	return stanceTable
end

--  TODO: Move this function to M:Stance?
---	Builds list of weapons that can equip a particlar stance mod as seen on [[Template:StanceWeapons]].
--	@function		p.getStanceWeaponList
--	@param			table} frame Frame object w/ first argument being string stanceName
--	@returns		{string} Resultant wikitext of comparison list
function p.getStanceWeaponList(frame)
	local stanceName = frame.args ~= nil and frame.args[1] or frame
	local Stance = StanceData[stanceName] or
		error('p.getStanceWeaponList(frame): '..stanceName..' not found')
	local pol = Stance.Polarity

	local result = {}
	for _, weap in ipairs(p._getMeleeWeapons(Stance.WeaponType, Stance.ConclaveOnly)) do
		table.insert(result, '*'
				..statFormat(weap, nil,'NameLink'):gsub('^%[%[',Stance.ConclaveOnly and '[[Conclave:' or '[[')
				..(weap.StancePolarity==pol and '&nbsp;✓' or '')
			)
	end
	return table.concat(result, '\n')
end

function p.getValue(frame)
	local weap, key, atk = table.unpack(frame.args)
	weap = p._getWeapon(weap)
	return p._getValue(weap, key, atk)
end
function p.getFormattedValue(frame)
	local weap, key, atk = table.unpack(frame.args)
	weap = p._getWeapon(weap)
	return p._getFormattedValue(weap, key, atk)
end

---	Builds a weapon gallery.
--	@function		getWeaponGallery
--	@param			{table} Weapons Array of weapon table entries to be displayed
--	@returns		{string} Resultant wikitext of gallery
-- local function getWeaponGallery(Weapons)
--	 local result = {'<gallery mode=packed>'}
--	 for i, weap in ipairs(Weapons) do
--		 table.insert(result, statRead(weap, nil,'Image')..'|'..statFormat(weap, nil,'NameLink'))
--	 end
--	 table.insert(result, '</gallery>')
--	 return table.concat(result, '\n')
-- end
--	 local result = {}
--	 -- local result = { '{| style="margin:auto;text-align:center;"' }
--	 -- local nameRow = {}
--	 for i, Weapon in ipairs(Weapons) do
--		 -- table.insert(result, ('|style="width:165px"|[[File:%s|150px|link=%s]]'):format(statRead(Weapon, nil,'Image'),statRead(Weapon, nil,'Link')))
--		 -- table.insert(nameRow, ('| style="vertical-align: text-top;" |%s'):format(statFormat(Weapon, nil,'NameLink')))
--		 table.insert(result, statFormat(Weapon, nil,'NameLink'))
--		 -- if i % 5 == 0 then
--		 --	 table.insert(result, table.concat(nameRow,'\n'))
--		 --	 table.insert(result, "|-")
--		 --	 nameRow = {}
--		 -- end
--	 end
--	 -- table.insert(result, table.concat(nameRow,'\n'))
--	 -- table.insert(result, '|}')
--	 return table.concat(result, '\n')
-- end

---	Builds a melee weapon gallery as seen on [[Template:MeleeCategory]].
--	@function		p.getMeleeWeaponGallery
--	@param			{table} frame Frame object w/ first argumenting being string meleeClass
--	@returns		{string} Resultant wikitext of gallery
function p.getMeleeWeaponGallery(frame)
	local meleeClass = frame.args[1] or ''
	local result = {"=="..meleeClass.." Weapons==",'<gallery widths="200" position="center" spacing="small">'}
	for i, weap in ipairs(p._getMeleeWeapons(meleeClass)) do
		table.insert(result, statRead(weap, nil,'Image')..'|'..statFormat(weap, nil,'Name'))
	end
	table.insert(result, '</gallery>')
	result = frame:preprocess(table.concat(result, '\n')) -- annoying that it needs to br preprocessed
	mw.log(result:gsub('%z','\\z'))
	return result
end
	-- return "=="..meleeClass.." Weapons==\n"..getWeaponGallery(p._getMeleeWeapons(meleeClass))

---	Gets the total count of weapons as used on [[Mastery Rank#Total Mastery]].
--	@function		p.getWeaponCount
--	@param			{table} frame Frame object w/ the first argument being the weaponSlot and the
--						  second argument being a boolean to getFullList
--	@returns		{number} Total count of weapons in a certain category/type
function p.getWeaponCount(frame)
	local weaponSlot = frame.args and frame.args[1] or frame
	local getFullList = frame.args and frame.args[2] or nil
	local count = 0
	local fullList = {}
	for name, weapon in Table.skpairs(WeaponData["Weapons"]) do
		if not weapon._IgnoreEntry then
			local wf = weapon.Slot == "Primary" or weapon.Slot == "Secondary" or weapon.Slot == "Melee"
			local ex = weapon.Class == "Exalted Weapon"
			local aw = string.find(weapon.Slot, "Arch")
			local mod = string.find(weapon.Class, "Kitgun") or string.find(weapon.Class, "Zaw")
			if weaponSlot == "All" or not ex and ((
				(not weaponSlot or weaponSlot == '')
				and not mod
				and weapon.Slot  ~= "Amp"
				and weapon.Class ~= "Ordnance"
				and weapon.Class ~= "Turret"
				and weapon.Class ~= "Unique"
				and not string.find(weapon.Slot, "Atmosphere")
			) or ({
				[weapon.Slot] = not mod,
				Warframe = wf,
				Archwing = aw,
				Rest = not wf and not aw,
			})[weaponSlot]) then
				if weapon.Name ~= "Dark Split-Sword (Heavy Blade)" then count = count + 1 end
				if getFullList then table.insert(fullList, '# '..name) end
			end
		end
	end
	if getFullList then
		table.sort(fullList, function(a,b)return a.Name<b.Name end)
		return table.concat(fullList, '\n'), count
	end
	return count
end

---	Gets the weapon class of weapons for use in comparison tables.
--	@function		p._getCategory
--	@param			{table} weapon Weapon table
--	@returns		{string} Category name, {string} Triggertype
function p._getCategory(weapon)
	local class = p._getValue(weapon, "Class")
	local showClass=({
		["Thrown"]=class,
		["Dual Shotguns"]=class,
		["Shotgun Sidearm"]=class,
		["Dual Pistols"]=class,
		["Pistol"]=class,
		["Shotgun"]=class,
		["Crossbow"]="Bow", ["Bow"]=class,
		["Sniper Rifle"]=class,
		["Launcher"]=class,
		["Arm Cannon"]=class,
		["Speargun"]=class,
		["Rifle"]=class,
	})[class] or "Other"
	return showClass, p._getValue(weapon, "Trigger")
end
p._getSecondaryCategory = p._getCategory
p._getPrimaryCategory = p._getCategory

---	Builds wikitable of all weapons' innate polarities as seen on [[Polarity]].
--	@function		p.getPolarityTable
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of resultant wikitable
function p.getPolarityTable(frame)
	local colNames = {'Primary','Secondary','Melee','Arch-Gun','Arch-Melee',}
	local cols = {}--{['Primary']={},['Secondary']={},['Melee']={},['Arch-Gun']={},['Arch-Melee']={},}
	local colOrder = {}--{cols['Primary'],cols['Secondary'],cols['Melee'],cols['Arch-Gun'],cols['Arch-Melee'],}
	local colCounts = {}

	for i,v in ipairs(colNames) do
		cols[v]={}
		colOrder[i]=cols[v]
		colCounts[v]=0
	end

	-- local entries = 
	p._getWeapons(function(weapon) 
		local pols = Table.size(weapon["Polarities"] or {})
		local slot = weapon['Slot']
		if pols>0 and cols[slot] then
			table.insert(cols[slot], {'|'..p._getFormattedValue(weapon,'NameLink'):gsub(' ?%(.*%)','')..'||'..p._getFormattedValue(weapon, "Polarities"),pols})
			colCounts[slot]=colCounts[slot]+1
		end
		-- return pols>0
	end)

	for i,v in ipairs(colNames) do
		colCounts[i]=colCounts[v]
		table.sort(cols[v],function(a,b)return a[2]>b[2] end)
	end

	local result = {[=[
{| style="width: 100%; border-collapse: collapse;" cellpadding="2" border="1"
|+ '''Weapons with Innate Polarities (ignoring Stance and Exilus slots)'''
! colspan="2" |Primaries
! colspan="2" |Secondaries
! colspan="2" |Melees
! colspan="2" |Arch-Guns
! colspan="2" |Arch-Melees]=]}
	for i=1,math.max(table.unpack(colCounts)) do --row
		table.insert(result, '|-')
		for _, col in ipairs(colOrder) do --cell
			table.insert(result,(col[i] or {'| ||'})[1])
		end
	end
	table.insert(result, '|}')
	return table.concat(result, '\n')
end

--- Builds comparison string between two values.
--	@function		compareStr
--	@param			{table} weap1 A table used to find the comparing values
--	@param			{table} atk1 A table used to find the comparing values
--	@param			{table} weap2 A table used to find the comparing values
--	@param			{table} atk2 A table used to find the comparing values
--	@param			{string} valName Name of statistic that values represent (e.g. "Critical Damage")
--	@param			{string} var Name of statistic that values are (e.g. "CritDamage")
--	@param[opt]	 {table}  compareAdjs Two element table that contains the greater than and less than comparative adjectives (e.g. {"Higher", "Lower", "Different"}) 
--	@param[opt]	 {string} prefix What to start the comparison string with if you want to increase the bullet level (e.g. "***")
--	@returns		{string} Resultant wikitext of comparison string (e.g. '**Higher Critical Damage (1.2x vs. 1.1x)')
local function compareStr(weap1, atk1, weap2, atk2, valName, var, compareAdjs, prefix)
	local val1,val2 = statRead(weap1,atk1,var), statRead(weap2,atk2,var)
	if val1 == val2 or val1 ~= val1 or val2 ~= val2 or not (val1 and val2) then return '' end

	if type(val1)=='table' then
		local a,b = val1, val2
		val1, val2 = Table.size(val1), Table.size(val2)
		if val1 == val2 and not (function()
			for k, v in pairs(a) do
				if b[k]~=v then
					return true
				end
			end
		end)() then
			return ''
		end
	end

	local adj = compareAdjs
	if type(compareAdjs)=='table' or not compareAdjs then
		adj = (compareAdjs or {"Higher","Lower","Different"})[
			val1==val2 and 3 or
			val1>val2 and 1 or
			val1<val2 and 2 or 0
		] or error('compareStr: vals are without compare: '..mw.dumpObject{val1=val1,val2=val2,compareAdjs=compareAdjs,adj=adj})
		-- local bigWord, smallWord = table.unpack(compareAdjs or {"Higher","Lower"})
		-- adj = bigWord~=smallWord and adj>0 and (bigWord or "Higher") or (smallWord or "Lower")
	end

	return ('%s %s %s (%s vs. %s)'):format(prefix or '**', adj, valName, statFormat(weap1,atk1,var), statFormat(weap2,atk2,var))
end

---	Builds damage comparison string between two attacks.
--	@function		damageComparisonStr
--	@param			{table} Attack1 Attack used for comparison
--	@param			{table} Attack2 Attack used to compare the first attack against
--	@returns		{string} Resultant wikitext of comparison string
local function damageComparisonStr(weap1, atk1, weap2, atk2)
	local result = {}
	-- TODO: Replace DAMAGE_TYPES with DAMAGE_ITERATION_ORDER table in M:DamageTypes
	-- ipairs iterates in the order given in DAMAGE_TYPES, so IPS is always first
	-- ^ i think this is better tho?
	for _, dt in ipairs(DAMAGE_TYPES) do
		result[#result+1] = compareStr(weap1, atk1, weap2, atk2, Tooltip.getFullTooltip(dt, 'DamageTypes').." damage", dt, nil, "***")
		-- local damage1 = ('%.2f'):format(Attack1.Damage[element] or 0)
		-- local damage2 = ('%.2f'):format(Attack2.Damage[element] or 0)
		-- result = result..bulidCompareString(damage1, damage2, Tooltip.getFullTooltip(element, 'DamageTypes').." damage", nil, nil, {"Higher", "Lower"}, "\n***")
	end
	return table.concat(result,'\n')
end

---	Builds comparison list between two weapons in PvE.
--	@function		p.buildComparison
--	@param			{table} frame Frame object
--	@returns		{string} Resultant wikitext of comparison list
function p.buildComparison(frame, getConclave)
	local weapon1Name = frame.args[1] or ''
	local weapon2Name = frame.args[2] or ''

	assert(weapon1Name ~= '' and weapon2Name ~= '', 'p.buildComparison(frame): Must compare two weapons')

	local weap1 = p._getWeapon(weapon1Name, getConclave)
	local weap2 = p._getWeapon(weapon2Name, getConclave)

	local atk1 = getAttack(weap1)
	local atk2 = getAttack(weap2)

	local result = {
		-- support method chaining w/ colon syntax
		insert = function(self, elem)
			table.insert(self, elem)
			return self
		end,
		insertcompare = function(self, ...)
			return self:insert(compareStr(weap1,atk1,weap2,atk2,...))
		end
	}

	result:insert(("* [[%s]] (%s), compared to [[%s]] (%s):"):format(
		weapon1Name,statRead(weap1,atk1,'AttackName'),
		weapon2Name,statRead(weap2,atk2,'AttackName'),
	nil))

	if getConclave then
		result[1]=result[1]:gsub('%[%[(.+)%]%]','[[Conclave:%1|%1]]')
	end

	local progenitorBonusNote = (
		string.find(weapon1Name, "Kuva") or string.find(weapon1Name, "Tenet") or
		string.find(weapon2Name, "Kuva") or string.find(weapon2Name, "Tenet") )
	and " (using max +60% [[Lich System/Progenitor|Progenitor]] bonus if applicable)" or ""

	result
		:insert(compareStr(weap1, atk1, weap2, atk2, "base damage per projectile", "BaseDamage"):gsub('^$','**Equal base damage, but different composition:'))
		:insert(damageComparisonStr(weap1, atk1, weap2, atk2))
		:insertcompare("total damage", "TotalDamage")
		:insertcompare("base [[critical chance]]", "CritChance")
		:insertcompare("base [[critical multiplier]]", "CritMultiplier")
		:insertcompare("base [[status chance]]", "StatusChance")
		:insertcompare("[[Damage#Final_Calculations|average damage per tap]]"..progenitorBonusNote, "AvgTapDmg")
		:insertcompare("[[Damage#Final_Calculations|burst DPS]]"..progenitorBonusNote, "BurstDps")
		:insertcompare("[[Damage#Final_Calculations|sustained DPS]]"..progenitorBonusNote, "SustainedDps")
	
	if statRead(weap1, atk1, "IsMelee") then
		result -- melee
			:insertcompare("Range", "MeleeRange", {"Longer","Shorter"})
			:insertcompare("[[attack speed]]", "FireRate")
			:insertcompare("[[Melee Combo|Combo Duration]]", "ComboDur")
			:insertcompare("Block Angle", "BlockAngle")
			:insertcompare("[[Stance]] Polarity", "StancePolarity", "Different")
	else
		result -- gun
			:insertcompare("starting [[Damage Falloff|damage falloff]] distance", "FalloffStart", {"Farther", "Closer"})
			:insertcompare("max [[Damage Falloff|damage falloff]] distance", "FalloffEnd", {"Farther", "Closer"})
			:insertcompare("max damage reduction at ending falloff distance", "FalloffReduction", {"Greater", "Lesser"})
			:insertcompare("[[fire rate]]", "FireRate")
			:insertcompare("[[multishot]]", "Multishot")
			:insertcompare("magazine", "Magazine", {"Larger", "Smaller"})
			:insertcompare("max ammo capacity", "MaxAmmo", {"Larger", "Smaller"})
			:insertcompare("[[Reload|reload time]]", "Reload", {"Slower", "Faster"})
			:insertcompare("spool-up", "Spool", {"Slower", "Faster"})
			:insertcompare("[[Accuracy|accurate]]", "Accuracy", {"More", "Less"})
	end
	
	result
		:insertcompare("[[Polarity|polarities]]", "Polarities", {"More", "Less", "Different"})
		:insertcompare("[[Mastery Rank]] required", "Mastery")
		:insertcompare("[[disposition]]", "Disposition")

	local se1, se2 = weap1.SyndicateEffect, weap2.SyndicateEffect
	if se1 and not se2 then
		result:insert("\n** Innate [["..se1.."]] effect")
	elseif se2 and not se1 then
		result:insert("\n** No innate [["..se2.."]] effect")
	elseif se1 and se2 and se1 ~= se2 then
		result:insert("\n** Different innate [[Syndicate Radial Effects|Syndicate Effect]] ([["..se1.."]] vs. [["..se2.."]])")
	end

	-- mw.log(mw.dumpObject{['M:Weapon cache']=cache})
	return table.concat(result, '\n'):gsub('\n\n+', '\n')..'[[Category:Automatic Comparison]]'
end

---	Builds comparison list between two weapons in PvP ([[Conclave]]).
--	@function		p.buildComparison
--	@param			{table} frame Frame object
--	@returns		{string} Resultant wikitext of comparison list
function p.buildConclaveComparison(frame)
	return buildComparison(frame, true)
end

---	Builds a table that lists out all weapons with a certain damage type
--	@function		p.buildDamageTypeTable
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of resultant wikitable
function p.buildDamageTypeTable(frame)
	local damageType = frame.args and frame.args[1] or frame
	local topPortion = frame.args and frame.args[2] or false

	local tRows = {}
	
	local weaponsTable = p._getWeapons(function(weap)
		-- Kitgun entries have 0 as placeholder damage values
		if string.find(weap['Class'], 'Kitgun') then
			return false
		elseif topPortion then
			return statRead(weap, nil, 'BiasType')==damageType
		end
		return (statRead(weap, nil, 'Damage')[damageType] or 0) > 0
	end)
	
	for i, weap in ipairs(weaponsTable) do
		local atk = getAttack(weap)
		local bias = {}
		if topPortion or statRead(weap, atk, 'BiasType') == damageType then
			bias[1] = statRead(weap, atk, 'BiasPortion')
			bias[2] = statFormat(weap, atk, 'DamageBias')
		else
			bias[1] = statRead(weap, atk, damageType)/statRead(weap, atk, 'BaseDamage')
			bias[2] = percent(bias[1])..dmgTooltip(damageType)
		end
		
		table.insert(tRows, ('| %s || %s || %s || %s || data-sort-value="%s" | %s'):format(
			Tooltip.getFullTooltip(statRead(weap, atk, 'Name'), 'Weapons'),
			statRead(weap, atk, 'Slot'),
			statRead(weap, atk, 'Class'),
			statRead(weap, atk, damageType),
			bias[1], bias[2],
		nil))
	end

	return ([[
{| class = "listtable sortable" style="margin:auto;"
|+ '''Weapons with %s%s damage'''
|-
! Name !! Slot !! Class !! data-sort-type="number" | %s !! data-sort-type="number" | %s%%
|-
]]):format(topPortion and 'mostly ' or '', damageType, Tooltip.getFullTooltip(damageType, 'DamageTypes'), damageType)
	..table.concat(tRows, '\n|-\n')..'\n|}'
end

---	Builds a list of weapons, with variants being next to base weapon name inside parentheses
--	(e.g. {{Weapon|Braton}} ({{Weapon|MK1-Braton|MK1}}, {{Weapon|Braton Prime|Prime}})).
--	@function		p._shortLinkList
--	@param			{table} Weapon Weapon table
--	@param			{boolean} tooltip If true, adds weapon tooltips, false otherwise; defaults to false
--	@returns		{string} Wikitext of resultant list
function p._shortLinkList(Weapons, tooltip)
	local baseNames = {}
	for key, weap in pairs(Weapons) do
		local isVar, varType, baseName = p._isVariant(weap.Name)
		if not baseNames[baseName] then baseNames[baseName] = {} end
		if not isVar then baseNames[baseName].hasBase = true end
		table.insert(baseNames[baseName], varType)
	end

	local link = tooltip and
	-- TODO: Replace function call with frame imitation with a new function that is used
	-- within modules. In other words, Tooltip.getFullTooltip() should not be used in both #invokes on articles
	-- and in modules.
	function(a, b) return Tooltip.getFullTooltip{ args = {a, 'Weapons', r = b} } end or
	function(a, b) return b and '[['..a..'|'..b..']]' or '[['..a..']]' end

	local result = {}
	for baseName, variants in Table.skpairs(baseNames) do
		local thisRow = {}
		for _, varName in ipairs(variants) do
			if varName ~= 'Base' and varName ~= '' then
				table.insert(thisRow, link(p._buildName(baseName, varName),variants.hasBase and varName))
			end
		end
		local bn = variants.hasBase and link(baseName) or ''
		local vars = #thisRow > 0 and (variants.hasBase and ' (%s)' or '%s'):format(table.concat(thisRow, ', ')) or ''
		table.insert(result, bn..vars)
	end
	return result
end

---	Builds a list of weapons' mastery requirements as seen on [[Template:EquipmentUnlock]],
--	[[Template:EquipmentUnlock/Primary]], [[Template:EquipmentUnlock/Secondary]], 
--	[[Template:EquipmentUnlock/Melee]], etc.
--	@function		p.getMasteryShortList
--	@param			{table} frame Frame object w/ first argument being a string weaponSlot
--	@returns		{string} Wikitext of resultant list
function p.getMasteryShortList(frame)
	local weaponSlot = frame.args[1]
	local masteryRank = tonumber(frame.args[2])
	local weapArray = p._getWeapons(function(x)
		return x.Slot == weaponSlot and x.Mastery == masteryRank
	end)
	return table.concat(p._shortLinkList(weapArray, true), ' • ')
end

---	Builds a list of PvP weapons as seen on [[PvP#Limitations]].
--	@function		p.getConclaveList
--	@param			{table} frame Frame object w/ first argument being a string weaponSlot
--	@returns		{string} Wikitext of resultant list
function p.getConclaveList(frame)
	local weaponSlot = frame.args[1] or 'All'
	-- local masteryRank = tonumber(frame.args[2])
	local weapArray = p._getWeapons(function(x)
		return (weaponSlot == 'All' or x.Slot == weaponSlot)-- and x.Mastery==masteryRank
	end, true)
	return '*'..table.concat(p._shortLinkList(weapArray, false), '\n* ')
end

---	Builds a disposition wikitable as seen on [[Riven Mods/Weapon Dispos]].
--	@function		p.getRivenDispositionTable
--	@param			{table} frame Frame object w/ first argument being a string weaponSlot
--	@returns		{string} Wikitext of resultant wikitable
function p.getRivenDispositionTable(frame)
	local weaponSlot = frame.args[1]
	local result = {
		'{| class="article-table" border="0" cellpadding="1" cellspacing="1" style="width: 100%"',
		'|-',
		{'[[a| '},	-- Wikitable header row
		'|-'
	}

	-- local ranges = {'○○○○○', '●○○○○', '●●○○○', '●●●○○', '●●●●○', '●●●●●'}
	local dispo = {}

	for k, weapon in pairs(WeaponData.Weapons) do
		if weapon['Disposition'] and (weaponSlot == 'All' or weapon['Slot'] == weaponSlot) then
			local disp = statFormat(weapon, nil, 'Dispo')
			dispo[disp] = dispo[disp] or {}
			table.insert(dispo[disp], weapon)
		end
	end

	dispo['○○○○○○'] = nil -- Grattler (Atmosphere) dispo is/was 0.1

	for str, dis in Table.skpairs(dispo) do
		table.sort(dis, function(a, b) return a['Disposition'] > b['Disposition'] end)
		local col = { '| style="vertical-align:top; font-size:small" |' }
		for _, weap in ipairs(dis) do
			table.insert(col, statFormat(weap, nil, 'NameLink')..' ('..weap['Disposition']..')')
		end
		table.insert(result[3], str)
		table.insert(result, table.concat(col, '\n* '))
	end

	result[3] = table.concat(result[3], ']]\n! scope="col" style="text-align:center;"|[[Riven Mods#Disposition|')..']]'
	table.insert(result, '|}')
	return table.concat(result, '\n')
end

return p
Advertisement