WARFRAME Wiki
WARFRAME Wiki
(p.buildDamageTypeTable(): replacing templates with native function calls to M:Tooltips)
(p.buildDamageTypeTable(): updated table name and documentation to reflect the fact that it only displays weapons that have the highest damage distribution of a certain damage type)
Line 1,458: Line 1,458:
 
end
 
end
   
--- Builds a table that lists out all weapons with a certain damage type
+
--- Builds a table that lists out all weapons with a certain damage type being
  +
-- the highest damage distribution out of all the damage types it deals
 
-- and the percentage that it makes up of their base damage of the attack specified
 
-- and the percentage that it makes up of their base damage of the attack specified
 
-- in their tooltip on the wiki.
 
-- in their tooltip on the wiki.
Line 1,482: Line 1,483:
 
local tHeader = string.format([[
 
local tHeader = string.format([[
 
{| class = "listtable sortable" style="margin:auto;"
 
{| class = "listtable sortable" style="margin:auto;"
|+ '''Weapons with %s damage'''
+
|+ '''Weapons with %s as highest damage distribution'''
 
|-
 
|-
 
! Name !! Slot !! Class !! %s !! data-sort-type="number" | %s%%
 
! Name !! Slot !! Class !! %s !! data-sort-type="number" | %s%%

Revision as of 18:58, 30 November 2021


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.csvGunComparisonTable(frame) (function)
Builds a CSV table of all WARFRAME's guns with the exception of Kitguns.
Parameter: frame Frame object (table)
Returns: Preformatted text of CSV text (string)
weapons.csvMeleeComparisonTable(frame) (function)
Builds a CSV table of all WARFRAME's melee with the exception of Zaws.
Parameter: frame Frame object (table)
Returns: Preformatted text of CSV text (string)
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._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._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._attackLoop(Weapon) (function)
Loops through all possible attacks that a weapon may have.
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._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, 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 /Conclave/data (table)
weapons._getWeapons(validateFunction, getConclave) (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)
Returns: Table of weapon table entries as seen in /data (table)
weapons._getValue(Weapon, keyName, attackName) (function)
Gets the raw value of a certain statistic of a weapon.
Parameters:
  • Weapon Weapon table (table)
  • keyName Name of key (string)
  • attackName 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.getStanceWeaponList(frame) (function)
Builds list of weapons that can equip a particlar stance mod as seen on Template:StanceWeapons.
Parameter: frame Frame object w/ first argument being string stanceName (table)
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._getSecondaryCategory(weapon) (function)
Gets the weapon class of secondary weapons for use in comparison tables.
Parameter: weapon Weapon table (table)
Returns: Category name, {string} Triggertype (string)
weapons._getPrimaryCategory(weapon) (function)
Gets the weapon class of primary 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.buildAutoboxCategories(frame) (function)
Adds weapon categories.
Parameter: frame Frame object (table)
Returns: Wikitext of category links (string)
weapons.buildDamageTypeTable(frame) (function)
Builds a table that lists out all weapons with a certain damage type being the highest damage distribution out of all the damage types it deals and the percentage that it makes up of their base damage of the attack specified in their tooltip on the wiki.
Parameter: frame Frame object (table)
Returns: Wikitext of resultant wikitable (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.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)
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.ppData(frame) (function)
Dumps a preprocessed table of weapon stats and info to copy into M:Weapons/ppdata. Invoked on Module:Weapons/ppdata/doc.
Parameter: frame Frame object (table)
Returns: ppdata preprocessed wikitext string in Lua table formatting (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:Flaicher|Flaicher]]
--	@attribution	[[User:FINNER|FINNER]]
--	@attribution	[[User:Cephalon Scientia|Cephalon Scientia]]
--	@attribution	[[User:Gigamicro|Gigamicro]]
--	@attribution	[[User:Synthtech|Synthtech]]
--	@attribution	[[User:Calenhed|Calenhed]]
--	@image		IconPrimaryWeaponRifle.png
--	@require	[[Module:Icon]]
--	@require	[[Module:Math]]
--	@require	[[Module:String]]
--	@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	stable
--	<nowiki>

local p = {}

local ConclaveData = mw.loadData [[Module:Weapons/Conclave/data]]
local ModData = mw.loadData [[Module:Mods/data]]
local ModularData = mw.loadData [[Module:Modular/data]]
local ppData = mw.loadData [[Module:Weapons/ppdata]]
local StanceData = mw.loadData [[Module:Stances/data]]
local WeaponData = mw.loadData [[Module:Weapons/data]]

local Icon = require [[Module:Icon]]
local Math = require [[Module:Math]]
local String = require [[Module:String]]
local Tooltip = require [[Module:Tooltips]]
local Version = require [[Module:Version]]
local Table = require [[Module:Table]]
local Polarity = require [[Module:Polarity]]

local WeapData = nil	-- Used in p.ppData() for processing a subset of WeaponData

-- TODO: Could use M:DamageTypes instead of this table?
local DAMAGE_TYPES = {
	"Impact", "Puncture", "Slash", "Heat", "Cold", "Toxin", 
	"Electricity", "Blast", "Corrosive", "Radiation", "Magnetic", "Gas", 
	"Viral", "Void", "True", "MinProgenitorBonus"
}

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

local GUN_KEY_MAP = {}
local ATTACK_KEY_MAP = {}

local function makeDTooltip(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

--- Returns the damage type with the highest damage distribution in an attack.
--	@function		getDamageBias
--	@param			{table} attackEntry Attack table
--	@returns		{number} Damage distribution as a decimal (e.g. 1 being 100%)
--	@returns		{string} Name of damage type with highest distribution
local function getDamageBias(attackEntry)
	if (attackEntry.Damage ~= nil and Table.size(attackEntry.Damage) > 0) then
		local totalDmg = 0
		local bestDmg = 0
		local bestElement = nil
		local elemCount = 0
		for damageType, dmg in pairs(attackEntry.Damage) do
			if (dmg > bestDmg) then
				bestDmg = dmg
				bestElement = damageType
			end
			totalDmg = totalDmg + dmg
			if (dmg > 0) then
				elemCount = elemCount + 1
			end
		end
		-- Make sure there are two damage instances that are above zero
		-- Exception for physical damage types
		if (elemCount > 0) then
			return (bestDmg / totalDmg), bestElement
		end
		error('getDamageBias(Attack): '..
			'Damage key in Attack entry has no key-value pairs'..mw.dumpObject(attackEntry))
	end
	error('getDamageBias(Attack): Attack entry has no Damage key')
end

--- Calculates the effective fire rate of a weapon's attack (not necessarily 
--	the same as base fire rate).
--	@function		calculateEffectiveFireRate
--	@param			{table} weaponEntry Weapon table data as seen in M:Weapons/data
--	@param[opt]		{string} attackName Name of attack key to pull attack stats from
--	@returns		{number} Effective fire rate in shots per second
local function calculateEffectiveFireRate(weaponEntry, attackName)
	if (attackName == nil) then
		attackName = weaponEntry['_TooltipAttackDisplay'] or 'Attack1'
	end
	local attackEntry = weaponEntry[attackName]
	-- Lanka has no delay between charged shots
	if (weaponEntry['Name'] == 'Lanka') then
		return 1 / attackEntry['ChargeTime']
	elseif (weaponEntry['Class'] == 'Bow') then
		if (attackEntry['Trigger'] == 'Charge') then
			return 1 / ( attackEntry['ChargeTime'] + weaponEntry['Reload'] )
		else
			return weaponEntry['Reload']	-- For uncharged bow attacks
		end
	end
	return 1 / ((attackEntry['ChargeTime'] or 0) + (1 / attackEntry['FireRate'] ) )
end

--- Calculates the derived damage stats commonly used in comparing gun performance.
--	Using what attacks the weapons tooltips are displaying for DPS calculations.
--	Assumes that Kuva/Tenet weapons have max Progenitor bonus (+60% bonus element).
--	@function		calculateGunDerivedDamage
--	@param			{table} Weapon Weapon table data as seen in M:Weapons/data
--	@param[opt]		{string} attackName Name of attack table to use for calculations
--	@returns		{number} Returns five number stats in a tuple: 
--				* Total damage: final damage when accounting multishot; same as arsenal display
--				* Average shot: damage per single input
--				* Average burst DPS: damage per second without reloading
--				* Average sustained DPS: damage per second w/ reloading
--				* Average lifetime damage: total damage that can be dealt in a single magazine + reserve ammo w/o picking up ammo drops
--				* Base DPS: damage per second without crits involved (for PvP)
local function calculateGunDerivedDamage(Weapon, attackName)
	local TooltipAttack = Weapon[attackName or Weapon['_TooltipAttackDisplay'] or 'Attack1'] or {}
	
	local totalDamage = 0
	
	for damageType, value in pairs(TooltipAttack['Damage'] or {}) do
		if (damageType == 'MinProgenitorBonus') then
			-- For a more competitive comparison, assume that Lich weapons have 
			-- max base damage bonus (+60% of a Progenitor Warframe's element);
			-- The value stored in 'MinProgenitorBonus' is based on the minimum +25% bonus
			totalDamage = totalDamage + (value * 0.6/0.25)	
		else
			totalDamage = totalDamage + value
		end
	end
	totalDamage = totalDamage * (TooltipAttack['Multishot'] or 1)
	
	-- TODO: Use GUN_KEY_MAP for CritChance and CritMultiplier instead?
	local critChance = TooltipAttack['CritChance'] or 0
	local critMultiplier = TooltipAttack['CritMultiplier'] or 0
	-- Fire rate also affects delay between charged shots; if charge time is zero
	-- effective fire rate is equal to base fire rate
	-- Bows do not have a fire rate which means that the limiting factor between shots
	-- is reload time
	local trueFireRate = calculateEffectiveFireRate(Weapon)
	local reloadTime = GUN_KEY_MAP['Reload'](Weapon)
	local magazine = GUN_KEY_MAP['Magazine'](Weapon)
	local maxAmmo = GUN_KEY_MAP['MaxAmmo'](Weapon)
	
	-- TODO: If we are defining average shot as average damage dealt per a single attack input
	-- than this is is inaccurate for burst-fire attacks (being picky on wording, but it is 
	-- for more accurate calculations)
	local avgShot = totalDamage * (1 + critChance * (critMultiplier - 1))
	-- Extra one needed in calculation to account for initial filled mag
	-- If there is no reserve ammo, that means that weapon can deal an infinite amount of damage theoretically
	local avgLifetimeDmg = maxAmmo < math.huge and avgShot * (magazine / (TooltipAttack['AmmoCost'] or 1)) * (1 + (maxAmmo / magazine)) or math.huge
	
	-- Need to ignore the first shot of guns since it is instantaneous and is 
	-- not affected by fire rate (which causes the delay between shots)
	local numShotPerMag = magazine / (TooltipAttack['AmmoCost'] or 1)
	local avgBurst = avgShot * trueFireRate
	local avgSustained
	
	if (Weapon['Name'] ~= 'Vectis' and Weapon['Name'] ~= 'Vectis Prime') then
		avgSustained = avgBurst * numShotPerMag / (trueFireRate * reloadTime + numShotPerMag)
	else
		avgSustained = avgBurst * numShotPerMag / (trueFireRate * reloadTime + numShotPerMag - 1)
	end
	
	-- Checking if avgSustained is NaN (i.e. 0/0); if so, then we can say that
	-- sustained damage is 'infinite'
	if (avgSustained ~= avgSustained) then avgSustained = math.huge end
	
	return totalDamage, avgShot, avgBurst, avgSustained, avgLifetimeDmg, (totalDamage * trueFireRate)
end

--- Calculates the average number of procs that occur on a single attack input.
--	@function		calculateAvgProcCount
--	@param			{table} weaponEntry Weapon table data as seen in M:Weapons/data
--	@param[opt]		{string} attackName Name of attack key to pull attack stats from
--	@returns		{number} Average number of procs on a single shot
local function calculateAvgProcCount(weaponEntry, attackName)
	if (attackName == nil) then
		attackName = weaponEntry['_TooltipAttackDisplay'] or 'Attack1'
	end
	local attackEntry = weaponEntry[attackName]
	local statusChance = attackEntry['StatusChance'] or 0
	local multishot = attackEntry['Multishot'] or 1
	local numForcedProcs = attackEntry['ForcedProcs'] and Table.size(attackEntry['ForcedProcs']) or 0
	return (statusChance + numForcedProcs) * multishot
end

-- Getter functions for attack keys and derived stats
local ATTACK_KEY_MAP = {
	Impact = function(attackEntry) return attackEntry['Damage']['Impact'] or 0 end,
	Puncture = function(attackEntry) return attackEntry['Damage']['Puncture'] or 0 end,
	Slash = function(attackEntry) return attackEntry['Damage']['Slash'] or 0 end,
	Cold = function(attackEntry) return attackEntry['Damage']['Cold'] or 0 end,
	Electricity = function(attackEntry) return attackEntry['Damage']['Electricity'] or 0 end,
	Heat = function(attackEntry) return attackEntry['Damage']['Heat'] or 0 end,
	Toxin = function(attackEntry) return attackEntry['Damage']['Toxin'] or 0 end,
	Blast = function(attackEntry) return attackEntry['Damage']['Blast'] or 0 end,
	Corrosive = function(attackEntry) return attackEntry['Damage']['Corrosive'] or 0 end,
	Gas = function(attackEntry) return attackEntry['Damage']['Gas'] or 0 end,
	Magnetic = function(attackEntry) return attackEntry['Damage']['Magnetic'] or 0 end,
	Radiation = function(attackEntry) return attackEntry['Damage']['Radiation'] or 0 end,
	Viral = function(attackEntry) return attackEntry['Damage']['Viral'] or 0 end,
	Void = function(attackEntry) return attackEntry['Damage']['Void'] or 0 end,
	MinProgenitorBonus = function(attackEntry) return attackEntry['Damage']['MinProgenitorBonus'] or 0 end,
	AttackName = function(attackEntry) return attackEntry['AttackName'] or 'Normal' end,
	AmmoCost = function(attackEntry) return attackEntry['AmmoCost'] or 1 end,
	BurstCount = function(attackEntry) return attackEntry['BurstCount'] end,
	BurstDelay = function(attackEntry) return attackEntry['BurstDelay'] end,
	BurstFireRate = function(attackEntry) return attackEntry['BurstFireRate'] end,
	BaseDamage = function(attackEntry)
		local total = 0
		for damageType, value in pairs(attackEntry['Damage']) do
			total = total + value
		end
		return total
	end,
	TotalDamage = function(attackEntry)
		local total = 0
		for damageType, value in pairs(attackEntry['Damage']) do
			total = total + value
		end
		return total * (attackEntry['Multishot'] or 1)
	end,
	DamageBias = function(attackEntry) return getDamageBias(attackEntry) end,
	ChargeTime = function(attackEntry) return attackEntry['ChargeTime'] or 0 end,
	CritChance = function(attackEntry) return attackEntry['CritChance'] end,
	CritMultiplier = function(attackEntry) return attackEntry['CritMultiplier'] end,
	ExtraHeadshotDmg = function(attackEntry) return attackEntry['ExtraHeadshotDmg'] or 0 end,
	FalloffEnd = function(attackEntry) return attackEntry['Falloff'] and attackEntry['Falloff']['EndRange'] or nil end,
	FalloffReduction = function(attackEntry) return attackEntry['Falloff'] and attackEntry['Falloff']['Reduction'] or nil end,
	FalloffStart = function(attackEntry) return attackEntry['Falloff'] and attackEntry['Falloff']['StartRange'] or nil end,
	FireRate = function(attackEntry) return attackEntry['FireRate'] end,
	ForcedProcs = function(attackEntry) return attackEntry['ForcedProcs'] end,
	HeadshotMultiplier = function(attackEntry)
		return attackEntry['HeadshotMultiplier'] and attackEntry['HeadshotMultiplier'] or 1
	end,
	Multishot = function(attackEntry) return attackEntry['Multishot'] or 1 end,
	PunchThrough = function(attackEntry) return attackEntry['PunchThrough'] or 0 end,
	Range = function(attackEntry) return attackEntry['Range'] end,
	ShotSpeed = function(attackEntry) return attackEntry['ShotSpeed'] or 'N/A' end,
	ShotType = function(attackEntry) return attackEntry['ShotType'] end,
	StatusChance = function(attackEntry) return attackEntry['StatusChance'] end,
}

-- The above map except returned values are formatted
local FORMATTED_ATTACK_KEY_MAP = {}
for funcName, func in pairs(ATTACK_KEY_MAP) do FORMATTED_ATTACK_KEY_MAP[funcName] = func end
FORMATTED_ATTACK_KEY_MAP['BurstDelay'] = function(attackEntry) return (ATTACK_KEY_MAP['BurstDelay'](attackEntry))..'s' end
FORMATTED_ATTACK_KEY_MAP['ChargeTime'] = function(attackEntry) return (ATTACK_KEY_MAP['ChargeTime'](attackEntry))..'s' end
FORMATTED_ATTACK_KEY_MAP['CritChance'] = function(attackEntry) return Math.percentage(ATTACK_KEY_MAP['CritChance'](attackEntry)) end
FORMATTED_ATTACK_KEY_MAP['CritMultiplier'] = function(attackEntry) return (ATTACK_KEY_MAP['CritMultiplier'](attackEntry))..'x' end
FORMATTED_ATTACK_KEY_MAP['DamageBias'] = function(attackEntry)
	local bestPercent, bestElement = ATTACK_KEY_MAP['DamageBias'](attackEntry)
	bestPercent = Math.percentage(bestPercent)
	return bestPercent..' '..makeDTooltip(bestElement)
end
FORMATTED_ATTACK_KEY_MAP['ExtraHeadshotDmg'] = function(attackEntry) return '+'..Math.percentage(ATTACK_KEY_MAP['ExtraHeadshotDmg'](attackEntry))..'%' end
FORMATTED_ATTACK_KEY_MAP['FalloffEnd'] = function(attackEntry)
	local v = ATTACK_KEY_MAP['FalloffEnd'](attackEntry)
	return v and v..'m' or 'N/A'
end
FORMATTED_ATTACK_KEY_MAP['FalloffReduction'] = function(attackEntry)
	local v = ATTACK_KEY_MAP['FalloffReduction'](attackEntry)
	return v and Math.percentage(v) or 'N/A'
end
FORMATTED_ATTACK_KEY_MAP['FalloffStart'] = function(attackEntry)
	local v = ATTACK_KEY_MAP['FalloffStart'](attackEntry)
	return v and v..'m' or 'N/A'
end
FORMATTED_ATTACK_KEY_MAP['ForcedProcs'] = function(attackEntry)
	local result = {}
	for _, forcedProc in ipairs(attackEntry['ForcedProcs'] or {}) do
		table.insert(result, forcedProc)
	end
	return table.concat(result, ', ')
end
FORMATTED_ATTACK_KEY_MAP['HeadshotMultiplier'] = function(attackEntry) return (ATTACK_KEY_MAP['HeadshotMultiplier'](attackEntry))..'x' end
FORMATTED_ATTACK_KEY_MAP['PunchThrough'] = function(attackEntry) return (ATTACK_KEY_MAP['PunchThrough'](attackEntry))..'m' end
FORMATTED_ATTACK_KEY_MAP['Radius'] = function(attackEntry) return (ATTACK_KEY_MAP['Radius'](attackEntry))..'m' end
FORMATTED_ATTACK_KEY_MAP['ShotSpeed'] = function(attackEntry) return (ATTACK_KEY_MAP['ShotSpeed'](attackEntry))..'m/s' end
FORMATTED_ATTACK_KEY_MAP['StatusChance'] = function(attackEntry) return Math.percentage(ATTACK_KEY_MAP['StatusChance'](attackEntry)) end

-- Getter functions for shared weapon keys and derived stats
local SHARED_KEY_MAP = {
	Class = function(weaponEntry) return weaponEntry['Class'] end,
	Disposition = function(weaponEntry)
		return weaponEntry['Disposition'] ~= nil and weaponEntry['Disposition'] or 'N/A'
	end,
	Family = function(weaponEntry) return weaponEntry['Family'] end,
	Introduced = function(weaponEntry)
		return weaponEntry['Introduced'] and Version._getVersion(weaponEntry['Introduced'])['Name'] or 'N/A'
	end,
	IntroducedDate = function(weaponEntry)
		return weaponEntry['Introduced'] and Version._getVersionDate(weaponEntry['Introduced']) or 'N/A'
	end,
	Link = function(weaponEntry) return '[['..weaponEntry['Link']..']]' end,
	Mastery = function(weaponEntry) return weaponEntry['Mastery'] or 'N/A' end,
	Name = function(weaponEntry) return weaponEntry['Name'] end,
	NameLink = function(weaponEntry) return '[['..weaponEntry['Link']..'|'..weaponEntry['Name']..']]' end,
	Polarities = function(weaponEntry) return Polarity._pols(weaponEntry['Polarities']) end,
	Traits = function(weaponEntry)
		local traitString = {}
		for _, trait in ipairs(weaponEntry['Traits'] or {}) do
			table.insert(traitString, trait)
		end
		return table.concat(traitString, ', ')
	end,
	Slot = function(weaponEntry) return weaponEntry['Slot'] end
}

-- Getter functions for gun weapon keys and derived stats
GUN_KEY_MAP = {
	Accuracy = function(weaponEntry)
		if (weaponEntry['Accuracy'] ~= nil) then return weaponEntry['Accuracy'] end
		return weaponEntry[weaponEntry['_TooltipAttackDisplay'] or 'Attack1']['Accuracy'] or 0
	end,
	AmmoType = function(weaponEntry)
		if (weaponEntry['AmmoType'] ~= nil) then
			return weaponEntry['AmmoType']
		elseif(weaponEntry['Slot'] == 'Secondary') then
			return 'Pistol'
		elseif(weaponEntry['Slot'] == 'Primary') then
			local class = Weapon['Class']
			if (class == 'Rifle') then
				return 'Rifle'
			elseif (class == 'Shotgun') then
				return 'Shotgun'
			elseif (class == 'Bow') then
				return 'Bow'
			elseif (class == 'Sniper Rifle' or class == 'Launcher') then
				return "Sniper"
			end
		end
		return ''
	end,
	AvgProcCount = function(weaponEntry, attackName)
		return calculateAvgProcCount(weaponEntry, attackName)
	end,
	AvgProcPerSec = function(weaponEntry, attackName)
		return calculateAvgProcCount(weaponEntry, attackName) * calculateEffectiveFireRate(weaponEntry, attackName)
	end,
	AvgShotDmg = function(weaponEntry, attackName)
		local _, avgShot = calculateGunDerivedDamage(weaponEntry, attackName)
		return avgShot
	end,
	BaseDps = function(weaponEntry, attackName)
		local _, _, _, _, _, baseDps = calculateGunDerivedDamage(weaponEntry, attackName)
		return baseDps
	end,
	BurstDps = function(weaponEntry, attackName)
		local _, _, avgBurst = calculateGunDerivedDamage(weaponEntry, attackName)
		return avgBurst
	end,
	EffectiveFireRate = function(weaponEntry, attackName)
		return calculateEffectiveFireRate(weaponEntry, attackName)
	end,
	ExilusPolarity = function(weaponEntry) return Polarity._polarity(weaponEntry['ExilusPolarity']) end,
	IsSilent = function(weaponEntry) return weaponEntry['IsSilent'] or false end,
	LifetimeDmg = function(weaponEntry, attackName)
		local _, _, _, _, avgLifetimeDmg = calculateGunDerivedDamage(weaponEntry, attackName)
		return avgLifetimeDmg
	end,
	Magazine = function(weaponEntry) return weaponEntry['Magazine'] or 0 end,
	MaxAmmo = function(weaponEntry) return weaponEntry['MaxAmmo'] or 0 end,
	Range = function(weaponEntry) return weaponEntry['Range'] end,
	Reload = function(weaponEntry) return weaponEntry['Reload'] or 0 end,
	ReloadDelay = function(weaponEntry) return weaponEntry['ReloadDelay'] or 0 end,
	ReloadDelayEmpty = function(weaponEntry) return weaponEntry['ReloadDelayEmpty'] or 0 end,
	ReloadStyle = function(weaponEntry) return weaponEntry['ReloadStyle'] or 'Magazine' end,
	Spool = function(weaponEntry) return weaponEntry['Spool'] or 0 end,
	SustainedDps = function(weaponEntry, attackName)
		local _, _, _, avgSustained = calculateGunDerivedDamage(weaponEntry, attackName)
		return avgSustained
	end,
	Trigger = function(weaponEntry)
		local attack = weaponEntry['_TooltipAttackDisplay'] or 'Attack1'
		return weaponEntry['Trigger'] or weaponEntry[attack]['Trigger'] or 'N/A'
	end
}
	
-- Getter functions for melee weapon keys and derived stats
local MELEE_KEY_MAP = {
	BlockAngle = function(weaponEntry) return weaponEntry['BlockAngle'] or 0 end,
	ComboDur = function(weaponEntry) return weaponEntry['ComboDur'] or 0 end,
	FollowThrough = function(weaponEntry) return weaponEntry['FollowThrough'] or 0 end,
	HeavyAttack = function(weaponEntry) return weaponEntry['HeavyAttack'] or 0 end,
	HeavyElement = function(weaponEntry) return weaponEntry['HeavyElement'] or 'Same as normal attack' end,
	HeavySlamAttack = function(weaponEntry) return weaponEntry['HeavySlamAttack'] end,
	HeavySlamElement = function(weaponEntry) return weaponEntry['HeavySlamElement'] or 'Same as normal attack' end,
	HeavyRadialDmg = function(weaponEntry) return weaponEntry['HeavyRadialDmg'] or 0 end,
	HeavyRadialElement = function(weaponEntry) return weaponEntry['HeavyRadialElement'] end,
	HeavySlamRadius = function(weaponEntry) return weaponEntry['HeavySlamRadius'] or 0 end,
	MeleeRange = function(weaponEntry) return weaponEntry['MeleeRange'] or 0 end,
	SlamAttack = function(weaponEntry) return weaponEntry['SlamAttack'] or 0 end,
	SlamElement = function(weaponEntry) return weaponEntry['SlamElement'] or 'Same as normal attack' end,
	SlamRadialDmg = function(weaponEntry) return weaponEntry['SlamRadialDmg'] or 0 end,
	SlamRadialElement = function(weaponEntry) return weaponEntry['SlamRadialElement'] end,
	SlamRadialProcs = function(weaponEntry) return weaponEntry['SlamRadialProcs'] end,
	SlamRadius = function(weaponEntry) return weaponEntry['SlamRadius'] or 0 end,
	SlideAttack = function(weaponEntry) return weaponEntry['SlideAttack'] or 0 end,
	SlideElement = function(weaponEntry) return weaponEntry['SlideElement'] or 'Same as normal attack' end,
	Stances = function(weaponEntry) return getWeaponStanceList(weaponEntry) end,
	StancePolarity = function(weaponEntry)
		return weaponEntry['StancePolarity'] ~= nil and
			Polarity._polarity(weaponEntry['StancePolarity']) or 'N/A'
	end,
	WindUp = function(weaponEntry) return weaponEntry['WindUp'] or 0 end
}

-- For mapping weapon traits or types to a category link
local CATEGORY_MAP = {
	Primary = '[[Category:Primary Weapons]]',
	Secondary = '[[Category:Secondary Weapons]]',
	Melee = '[[Category:Melee Weapons]]',
	['Arch-Melee'] = '[[Category:Archwing Melee]]',
	['Arch-Gun'] = '[[Category:Archwing Gun]]',
	['Arch-Gun (Atmosphere)'] = '[[Category:Archwing Gun]]',
	Kitgun = '[[Category:Kitgun]]',
	Zaw = '[[Category:Zaw]]',
	['Railjack Turret'] = '[[Category:Railjack]]',
	['Railjack Armament'] = '[[Category:Railjack]]',
	Gear = '[[Category:Gear]]',
	
	Rifle = '[[Category:Assault Rifle]]',
	['Sniper Rifle'] = '[[Category:Sniper Rifle]]',
	Shotgun = '[[Category:Shotgun]]',
	Pistol = '[[Category:Pistol]]',
	['Dual Pistols'] = '[[Category:Dual Pistols]]',
	Bow = '[[Category:Bow]]',
	Launcher = '[[Category:Launcher]]',
	['Arm-Cannon'] = '[[Category:Arm-Cannon]]',
	['Speargun'] = '[[Category:Speargun]]',
	Thrown = '[[Category:Thrown]]',
	['Shotgun Sidearm'] = '[[Category:Shotgun Sidearm]]',
	
	Prime = '[[Category:Prime Weapons]]',
	['Never Vaulted'] = '[[Category:Never Vaulted]]',
	Vaulted = '[[Category:Vaulted]]',
	Wraith = '[[Category:Wraith]]',
	Vandal = '[[Category: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'] = '[[Category:Kuva Lich]]',
	Prisma = '[[Category:Prisma]]',
	
	Grineer = '[[Category:Grineer Weapons]]',
	Corpus = '[[Category:Corpus Weapons]]',
	Infested = '[[Category:Infested Weapons]]',
	Tenno = '[[Category:Tenno Weapons]]',
	Sentient = '[[Category:Sentient Weapons]]',
	Entrati = '[[Category:Entrati Weapons]]',
	Baro = '[[Category:Baro Ki\'Teer Offering]]',
	Syndicate = '[[Category:Syndicate Offerings]]',
	['Invasion Reward'] = '[[Category:Invasion Reward]]',
	
	['Alt Fire'] = '[[Category:Weapons with Alt Fire]]',
	['AoE'] = '[[Category:Weapons with Area of Effect]][[Category:Self Interrupt Weapons]]',
	
	Active = '[[Category:Active]]',
	Auto = '[[Category:Automatic]]',
	['Auto-Spool'] = '[[Category:Automatic]]',
	Burst = '[[Category:Burst Fire]]',
	['Auto-Burst'] = '[[Category:Burst Fire]]',
	['Auto Charge'] = '[[Category:Charge]]',
	Charge = '[[Category:Charge]]',
	Duplex = '[[Category:Duplex Fire]]',
	['Semi-Auto'] = '[[Category:Semi-Automatic]]',
	Held = '[[Category:Continuous Weapons]]'
}

---	Builds a CSV table of all WARFRAME's guns with the exception of Kitguns.
--	@function		p.csvGunComparisonTable
--	@param			{table} frame Frame object
--	@returns		{string} Preformatted text of CSV text
function p.csvGunComparisonTable(frame)
	-- Weapon types to show in resultant table
	local weaponSlotFilter = { 'Primary', 'Secondary', 'Robotic', 'Arch-Gun', 'Arch-Gun (Atmosphere)', 'Amp' }
	local tableEntryTemplate = {}	-- Would look like '%s,%s,%s'
	-- Header names will also be key names to getter function maps
	local tableHeader = { 
		'Name',
		'Trigger',
		'AttackName',
		'Impact',
		'Puncture',
		'Slash',
		'Cold',
		'Electricity',
		'Heat',
		'Toxin',
		'Blast',
		'Corrosive',
		'Gas',
		'Magnetic',
		'Radiation',
		'Viral',
		'Void',
		'MinProgenitorBonus',
		'BaseDamage',
		'TotalDamage',
		'CritChance',
		'CritMultiplier',
		'AvgShotDmg',
		'BurstDps',
		'SustainedDps',
		'LifetimeDmg',
		'StatusChance',
		'ForcedProcs',
		'AvgProcCount',
		'AvgProcPerSec',
		'Multishot',
		'FireRate',
		'Disposition',
		'Mastery',
		'Magazine',
		'MaxAmmo',
		'Reload',
		'ShotType',
		'PunchThrough',
		'Accuracy',
		'Introduced',
		'IntroducedDate',
		'Slot',
		'Class',
		'AmmoType',
		'Range'
	}
	
	for i, _ in ipairs(tableHeader) do table.insert(tableEntryTemplate, '%s,') end
	tableEntryTemplate[#tableEntryTemplate] = '%s'	-- Last column
	tableEntryTemplate = table.concat(tableEntryTemplate)
	
	local csvResult = { '<pre>' }
	table.insert(csvResult, string.format(tableEntryTemplate, unpack(tableHeader)))
	
	for weaponName, weaponEntry in Table.skpairs(WeaponData['Weapons']) do
		if (Table.contains(weaponSlotFilter, weaponEntry['Slot']) and 
				string.find(weaponEntry['Class'], 'Kitgun') == nil and 
				not weaponEntry['_IgnoreEntry']) then
			-- Going through all the possible Attack keys and adding them to CSV
			-- (TODO: consolidate these keys into one Attack table with key-value pairs)
			for i = 1, 9, 1 do
				if (weaponEntry['Attack'..i] ~= nil) then
					local tableEntryValues = {}
					local attackName = 'Attack'..i
					
					for _, keyName in ipairs(tableHeader) do
						local v = p._getValue(weaponEntry, keyName, attackName)
						-- Serializing table to a string
						if (type(v) == 'table') then
							v = "'"..mw.dumpObject(v):gsub('%s+metatable = table#%d+', ''):gsub('table#%d+ ', '').."'"
						end
						table.insert(tableEntryValues, tostring(v))
					end
					
					local tableEntry = string.format(tableEntryTemplate, unpack(tableEntryValues))
					table.insert(csvResult, tableEntry)
				end
			end
		end
	end
	
	table.insert(csvResult, '</pre>')
	return table.concat(csvResult, '\n')
end

---	Builds a CSV table of all WARFRAME's melee with the exception of Zaws.
--	@function		p.csvMeleeComparisonTable
--	@param			{table} frame Frame object
--	@returns		{string} Preformatted text of CSV text
function p.csvMeleeComparisonTable(frame)
	-- Weapon types to show in resultant table
	local weaponSlotFilter = { 'Melee' }
	local tableEntryTemplate = {}	-- Would look like '%s,%s,%s'
	-- Header names will also be key names to getter function maps
	local tableHeader = { 
		'Name',
		'AttackName',
		'Impact',
		'Puncture',
		'Slash',
		'Cold',
		'Electricity',
		'Heat',
		'Toxin',
		'Blast',
		'Corrosive',
		'Gas',
		'Magnetic',
		'Radiation',
		'Viral',
		'Void',
		'MinProgenitorBonus',
		'BaseDamage',
		'TotalDamage',
		'CritChance',
		'CritMultiplier',
		'AvgShotDmg',
		'StatusChance',
		'ForcedProcs',
		'AvgProcCount',
		'FireRate',
		'Disposition',
		'Mastery',
		'Introduced',
		'IntroducedDate',
		'Slot',
		'Class',
		'MeleeRange',
		'HeavyAttack',
		'HeavySlamAttack',
		'HeavyRadialDmg',
		'HeavySlamRadius',
		'WindUp',
		'SlamAttack',
		'SlamRadialDmg',
		'SlamRadialElement',
		'SlamRadialProcs',
		'SlamRadius',
		'SlideAttack',
		'SlideElement'
	}
	
	for i, _ in ipairs(tableHeader) do table.insert(tableEntryTemplate, '%s,') end
	tableEntryTemplate[#tableEntryTemplate] = '%s'	-- Last column
	tableEntryTemplate = table.concat(tableEntryTemplate)
	
	local csvResult = { '<pre>' }
	table.insert(csvResult, string.format(tableEntryTemplate, unpack(tableHeader)))
	
	for weaponName, weaponEntry in Table.skpairs(WeaponData['Weapons']) do
		if (Table.contains(weaponSlotFilter, weaponEntry['Slot']) and 
				string.find(weaponEntry['Class'], 'Zaw') == nil and 
				not weaponEntry['_IgnoreEntry']) then
			-- Going through all the possible Attack keys and adding them to CSV
			-- (TODO: consolidate these keys into one Attack table with key-value pairs)
			for i = 1, 9, 1 do
				if (weaponEntry['Attack'..i] ~= nil) then
					local tableEntryValues = {}
					local attackName = 'Attack'..i
					
					for _, keyName in ipairs(tableHeader) do
						local v = p._getValue(weaponEntry, keyName, attackName)
						-- Serializing table to a string
						if (type(v) == 'table') then
							v = "'"..mw.dumpObject(v):gsub('%s+metatable = table#%d+', ''):gsub('table#%d+ ', '').."'"
						end
						table.insert(tableEntryValues, tostring(v))
					end
					
					local tableEntry = string.format(tableEntryTemplate, unpack(tableEntryValues))
					table.insert(csvResult, tableEntry)
				end
			end
		end
	end
	
	table.insert(csvResult, '</pre>')
	return table.concat(csvResult, '\n')
end

---	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
			local baseName = string.gsub(weaponName, " ?"..var.." ?-?", "")
			return true, var, baseName
		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 (variant == nil or variant == 'Base' or variant == '') then
		return baseName
	elseif (baseName == 'Laser Rifle' and variant == 'Prime') then
		return variant..' '..baseName	-- Laser Rifle has a primed version called 'Prime Laser Rifle'
	elseif (variant == 'Prime' or variant == 'Wraith' or variant == 'Vandal') then
		return baseName..' '..variant
	elseif (variant == 'MK1') then
		return 'MK1-'..baseName
	end
	return variant..' '..baseName
end

-- TODO: Function can be refactored
---	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)
	-- TODO: Isn't an easier way to check if a weapon is a variant is using the
	-- Family key which contains the name the base weapon and just gsub out that portion?
	-- First grabbing all the pieces and stashing them in a table
	local baseNames = {}
	for key, weap in Table.skpairs(Weapons) do
		local isVar, varType, baseName = p._isVariant(weap.Name)
		if (baseNames[baseName] == nil) then baseNames[baseName] = {} end
		table.insert(baseNames[baseName], varType)
	end
	
	-- Then the fun part: Pulling the table together
	local result = {}
	for baseName, variants in Table.skpairs(baseNames) do
		-- So first, check if "Base" is in the list
		-- Because if it isn't, list all variants separately
		if (Table.contains(variants, "Base")) then
			table.sort(variants)
			-- First, get the basic version
			local thisRow = ""
			if (tooltip) then
				thisRow = "{{Weapon|"..baseName.."}}"
			else
				thisRow = "[["..baseName.."]]"
			end
			-- Then, if there are variants...
			if (Table.size(variants) > 1) then
				-- List them in parentheses one at a time
				thisRow = thisRow.." ("
				local count = 0
				for i, varName in pairs(variants) do
					if (varName ~= "Base") then
						if (count > 0) then thisRow = thisRow..", " end
						if (tooltip) then
							thisRow = thisRow.."{{Weapon|"..p._buildName(baseName, varName).."|"..varName.."}}"
						else
							thisRow = thisRow.."[["..p._buildName(baseName, varName).."|"..varName.."]]"
						end
						count = count + 1
					end
				end
				thisRow = thisRow..")"
			end
			table.insert(result, thisRow)
		else
			for i, varName in pairs(variants) do
				if (tooltip) then
					table.insert(result, "{{Weapon|"..p._buildName(baseName, varName).."}}")
				else
					table.insert(result, "[["..p._buildName(baseName, varName).."]]")
				end
			end
		end
	end
	return result
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)
	local weapon = getConclave and ConclaveData["Weapons"][weaponName] or WeaponData["Weapons"][weaponName]
	if weapon ~= nil then return weapon end
	error('p._getWeapon(weaponName, getConclave): "'..weaponName..
		'" does not exist in [[Module:Weapons/data]] or [[Module:Weapons/Conclave/data]]')
end

---	Returns a specific attack table from a weapon table entry.
--	@function		getAttack
--	@param			{table, string} Weapon Weapon entry as seen in <code>/data</code> or the name of weapon
--	@param[opt]		{string} attackName Name of attack key; if omitted, will default to "Attack1"
--	@returns		{table} Attack table
local function getAttack(Weapon, attackName)
	if (Weapon == nil or attackName == nil) then return end
	if (type(Weapon) == "string") then
		Weapon = p._getWeapon(Weapon)
	end
	if (not attackName) then
		return Weapon.Attack1 or Weapon.Damage and Weapon
	end
	return Weapon[attackName:find 'Attack' and attackName or 'Attack'..attackName]
end

---	Loops through all possible attacks that a weapon may have.
--	@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 (Weapon == nil) 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

-- TODO: Remove this function and use p._getWeapons() w/ a validation function that filters
-- out melee weapon entries?
---	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)
	local weaps = {}
	for i, weap in Table.skpairs(WeaponData["Weapons"]) do
		if ((weap.Ignore == nil or not weap.Ignore) and weap.Slot ~= nil and weap.Slot == "Melee" and not weap._IgnoreEntry) then
			local classMatch = (weapClass == nil or weap.Class == weapClass)
			local pvpMatch = (PvP == nil or (PvP and weap.Conclave ~= nil and weap.Conclave))
			if (classMatch and pvpMatch) then
				table.insert(weaps, weap)
			end
		end
	end
	
	return weaps
end

-- TODO: Can probably be removed and refactored into getMeleeWeapons()
---	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._getConclaveMeleeWeapons
--	@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>/Conclave/data</code>
function p._getConclaveMeleeWeapons(weapClass, PvP)
	local weaps = {}
	local weapClasses = {}
	if (weapClass ~= nil) then
		weapClasses = String.split(weapClass, ",")
	end
	
	for i, weap in Table.skpairs(ConclaveData["Weapons"]) do
		if ((weap.Ignore == nil or not weap.Ignore) and weap.Slot ~= nil and weap.Slot == "Melee") then
			local classMatch = (weapClass == nil or Table.contains(weapClasses, weap.Class))
			local pvpMatch = (PvP == nil or (PvP and weap.Conclave ~= nil and weap.Conclave))
			if (classMatch and pvpMatch) then
				table.insert(weaps, weap)
			end
		end
	end
	
	return weaps
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
--	@returns		{table} Table of weapon table entries as seen in <code>/data</code>
function p._getWeapons(validateFunction, getConclave)
	local weaponList = {}
	for weaponName, weaponEntry in Table.skpairs(getConclave and ConclaveData["Weapons"] or WeaponData["Weapons"]) do
		if (validateFunction(weaponEntry) and not weaponEntry['_IgnoreEntry'] ) then
			table.insert(weaponList, weaponEntry)
		end
	end
	return weaponList
end

-- TODO: Move to M:Stances?
---	Gets stance mods for a particular melee class.
--	@function		getWeaponStanceList
--	@param			{string} meleeClass Melee class
--	@param			{boolean} pvpOnly 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, pvpOnly)
	local stanceTable = {}
	for stanceName, Stance in pairs(StanceData) do
		local typeMatch = (meleeClass == nil or meleeClass == Stance.WeaponType)
		local pvpMatch = (pvpOnly ~= nil and pvpOnly) or (Stance.ConclaveOnly == nil or not Stance.ConclaveOnly)
		if (typeMatch and pvpMatch) then
			stanceTable[stanceName] = Stance
		end
	end
	return stanceTable
end

---	Gets the raw value of a certain statistic of a weapon.
--	@function		p._getValue
--	@param			{table} Weapon Weapon table
--	@param			{string} keyName Name of key
--	@param[opt]		{string} attackName Name of attack to search through; defaults to 'Attack1' or what '_TooltipAttackDisplay' is set to
--	@returns		{string, number} Value of statistic
function p._getValue(Weapon, keyName, attackName)
	if (attackName == nil) then	attackName = Weapon['_TooltipAttackDisplay'] or 'Attack1' end
	local status, result
	-- Doing it this way allows returning of nil values
	for _, map in ipairs({ SHARED_KEY_MAP, GUN_KEY_MAP, MELEE_KEY_MAP }) do
		if (map[keyName] ~= nil) then
			status, result = pcall(map[keyName], Weapon, attackName)
			if (status) then return result end
		end
	end
	if (ATTACK_KEY_MAP[keyName] ~= nil) then
		status, result = pcall(ATTACK_KEY_MAP[keyName], Weapon[attackName])
		if (status) then return result end
	end
	-- If result == nil after searching through SHARED_KEY_MAP, GUN_KEY_MAP, MELEE_KEY_MAP, ATTACK_KEY_MAP
	-- that means keyName is not in one of these maps containing getter functions
	error('p._getValue(Weapon, keyName, attackName): Cannot get keyName "'..keyName..'" for "'..attackName..
		'" in '..mw.dumpObject(Weapon)..' Error message: '..
		(result == nil and '"'..keyName..'" not supported in either SHARED_KEY_MAP, GUN_KEY_MAP, MELEE_KEY_MAP, or ATTACK_KEY_MAP'or result))
	
	-- Original return statement with fallback calls
	-- return SHARED_KEY_MAP[keyName] ~= nil and SHARED_KEY_MAP[keyName](Weapon, attackName) or
	-- 	GUN_KEY_MAP[keyName] ~= nil and GUN_KEY_MAP[keyName](Weapon, attackName) or 
	-- 	MELEE_KEY_MAP[keyName] ~= nil and MELEE_KEY_MAP[keyName](Weapon, attackName) or
	-- 	ATTACK_KEY_MAP[keyName] ~= nil and ATTACK_KEY_MAP[keyName](Weapon[attackName]) or
	-- 	error('p._getValue(Weapon, keyName, attackName): Cannot get keyName "'..keyName..'" in '..mw.dumpObject(Weapon))
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(Weapon, keyName, attackName)
	if (attackName == nil) then attackName = Weapon['_TooltipAttackDisplay'] or 'Attack1' end
	
	return SHARED_KEY_MAP[keyName] ~= nil and SHARED_KEY_MAP[keyName](Weapon, attackName) or
		GUN_KEY_MAP[keyName] ~= nil and GUN_KEY_MAP[keyName](Weapon, attackName) or 
		MELEE_KEY_MAP[keyName] ~= nil and MELEE_KEY_MAP[keyName](Weapon, attackName) or
		FORMATTED_ATTACK_KEY_MAP[keyName] ~= nil and FORMATTED_ATTACK_KEY_MAP[keyName](Weapon[attackName]) or
		error('p._getFormattedValue(Weapon, keyName, attackName): Cannot get keyName "'..keyName..'" with attackName "'..attackName..'" in '..mw.dumpObject(Weapon))
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]
	assert(Stance ~= nil, 'p.getStanceWeaponList(frame): '..stanceName..' not found')
	
	local weaps = p._getMeleeWeapons(Stance.WeaponType, Stance.ConclaveOnly)
	local result = {}
	
	for i, weap in Table.skpairs(weaps) do
		local listItem = '*[['..weap.Name..']]'
		if (Stance.ConclaveOnly) then
			listItem = '*[[Conclave:'..weap.Name..'|'..weap.Name..']]'
		end
		
		if (weap.StancePolarity == ModData["Mods"][stanceName].Polarity) then
			listItem = listItem..'&nbsp;✓'
		end
		table.insert(result, listItem)
	end
	
	return table.concat(result, '\n')
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 = { '{| style="margin:auto;text-align:center;"' }
	local nameRow = ""
	for i, Weapon in pairs(Weapons) do
		local image = Weapon.Image ~= nil and Weapon.Image or "Panel.png"
		if ((i - 1) % 5 == 0) then 
			table.insert(result, nameRow.."\n|-") 
			nameRow = "\n|-"
		end
		table.insert(result, '| style="width:165px" |[[File:'..image..'|150px|link='..Weapon.Name..']]')
		nameRow = nameRow..'\n| style="vertical-align: text-top;" |[['..Weapon.Name..']]'
	end
	table.insert(result, nameRow)
	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 ~= nil and frame.args[1] or ""
	local WeapArray = p._getMeleeWeapons(meleeClass)
	local result = "=="..meleeClass.." Weapons==\n"
	result = result..getWeaponGallery(WeapArray)
	return result
end

---	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 ~= nil and frame.args[1] or nil
	local getFullList = frame.args ~= nil 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
			if weaponSlot == "All" then -- All weapons
				count = count + 1
				if getFullList then table.insert(fullList, '# '..name) end
			elseif not(weaponSlot) or weaponSlot == "" then -- Normal weapons (no Exalted)
				if not(weapon.Slot == "Amp" or string.find(weapon.Class, "Kitgun")
					or string.find(weapon.Class, "Zaw") or weapon.Class == "Ordnance"
					or weapon.Class == "Turret" or weapon.Class == "Unique"
					or weapon.Class == "Exalted Weapon" or string.find(weapon.Slot, "Atmosphere")) then
					count = count + 1
					if getFullList then table.insert(fullList, '# '..name) end
				end
			elseif weaponSlot == "Warframe" then -- Warframe weapons (no Exalted)
				if (weapon.Slot == "Primary" or weapon.Slot == "Secondary" or weapon.Slot == "Melee") and weapon.Class ~= "Exalted Weapon" then
					count = count + 1
					if getFullList then table.insert(fullList, '# '..name) end
				end
			elseif weaponSlot == "Archwing" then -- Archwing weapons (no Exalted)
				if (weapon.Slot == "Arch-Gun" or weapon.Slot == "Arch-Melee") and weapon.Class ~= "Exalted Weapon" then
					count = count + 1
					if getFullList then table.insert(fullList, '# '..name) end
				end
			elseif weaponSlot == "Rest" then -- Other weapons (no Exalted)
				if weapon.Slot ~= "Arch-Gun" and weapon.Slot ~= "Arch-Melee" and weapon.Slot ~= "Primary" and 
						weapon.Slot ~= "Secondary" and weapon.Slot ~= "Melee" and weapon.Class ~= "Exalted Weapon" then
					count = count + 1
					if getFullList then table.insert(fullList, '# '..name) end
				end
			elseif weapon.Slot == weaponSlot and weapon.Class ~= "Exalted Weapon" and string.find(weapon.Class, "Kitgun") == nil then
				count = count + 1
				if getFullList then table.insert(fullList, '# '..name) end
			end
		end
	end
	if getFullList then return table.concat(fullList, '\n') end
	if not(weaponSlot) or weaponSlot == "" or weaponSlot == 'Melee' then
		count = count - 1
	end
	-- Need to subtract 1 from melee count due to Dark Split-Sword having two different forms
	return count
end

---	Gets the weapon class of secondary weapons for use in comparison tables.
--	@function		p._getSecondaryCategory
--	@param			{table} weapon Weapon table
--	@returns		{string} Category name, {string} Triggertype
function p._getSecondaryCategory(weapon)
	local class = p._getValue(weapon, "Class")
	local trigger = p._getValue(weapon, "Trigger")
	if (class == "Thrown") then	return "Thrown", trigger
	elseif (class == "Dual Shotguns") then return "Dual Shotguns", trigger
	elseif (class == "Shotgun Sidearm") then return "Shotgun Sidearm", trigger
	elseif (class == "Dual Pistols") then return "Dual Pistols", trigger
	elseif (class == "Pistol") then return "Pistol", trigger
	end
	return "Other", trigger
end

---	Gets the weapon class of primary weapons for use in comparison tables.
--	@function		p._getPrimaryCategory
--	@param			{table} weapon Weapon table
--	@returns		{string} Category name, {string} Triggertype
function p._getPrimaryCategory(weapon)
	local class = p._getValue(weapon, "Class")
	local trigger = p._getValue(weapon, "Trigger")
	if (class == "Shotgun") then return "Shotgun", trigger
	elseif (class == "Bow" or class == "Crossbow") then return "Bow", trigger
	elseif (class == "Sniper Rifle") then return "Sniper", trigger
	elseif (class == "Launcher") then return "Launcher", trigger
	elseif (class == "Arm Cannon") then return "Arm Cannon", trigger
	elseif (class == "Speargun") then return "Speargun", trigger
	elseif (class == "Rifle") then	return "Rifle", trigger
	end
	return "Other", trigger
end

---	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 tableResult = { [[
{| 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]] }
	
	local filterBy = function(weaponSlot)
		return function(weaponEntry) 
			return (weaponEntry['Slot'] == weaponSlot) and string.len(p._getValue(weaponEntry, "Polarities")) > 0
		end
	end
	
	local meleeEntries = p._getWeapons(filterBy('Melee'))
	local pistolEntries = p._getWeapons(filterBy('Secondary'))
	local primaryEntries = p._getWeapons(filterBy('Primary'))
	local archGunEntries = p._getWeapons(filterBy('Arch-Gun'))
	local archMeleeEntries = p._getWeapons(filterBy('Arch-Melee'))
	
	local meleeCount = Table.size(meleeEntries)
	local pistolCount = Table.size(pistolEntries)
	local primaryCount = Table.size(primaryEntries)
	local archGunCount = Table.size(archGunEntries)
	local archMeleeCount = Table.size(archMeleeEntries)
	
	local maxLen = meleeCount
	if (pistolCount > maxLen) then maxLen = pistolCount end
	if (primaryCount > maxLen) then maxLen = primaryCount end
	if (archGunCount > maxLen) then maxLen = archGunCount end
	if (archMeleeCount > maxLen) then maxLen = archMeleeCount end
	
	local traverseOrder = { primaryCount, pistolCount, meleeCount, archGunCount, archMeleeCount }
	-- Note: Cannot map tables to their size since it would exceed allocated script time
	-- to access these
	local countEntries = {
		[primaryCount] = primaryEntries,
		[pistolCount] = pistolEntries,
		[meleeCount] = meleeEntries,
		[archGunCount] = archGunEntries,
		[archMeleeCount] = archMeleeEntries 
	}
	
	for i = 1, maxLen, 1 do
		table.insert(tableResult, '|-')
		-- Adding each row in table
		for _, entriesCount in ipairs(traverseOrder) do
			if (i <= entriesCount) then
				table.insert(tableResult, '| [['..countEntries[entriesCount][i]['Name']..']] ||'..Polarity._pols(countEntries[entriesCount][i]['Polarities']))
			else
				table.insert(tableResult, '| ||')
			end
		end
	end
	table.insert(tableResult, '|}')
	return table.concat(tableResult, '\n')
end

---	Builds comparison string between two values.
--	@function		p.buildCompareString
--	@param			{string, number} firstVal Value used for comparison
--	@param			{string, number} secondVal Value used to compare the first value against
--	@param			{string} valName Name of statistic that values represent (e.g. "Critical Damage")
--	@param[opt]		{number} digits The decimal to round the values by (e.g. if you want to round to two decimal places put in 0.01)
--	@param[opt]		{string} unit The values' unit (e.g. "m" or "seconds")
--	@param[opt]		{table} compareAdjs Two element table that contains the greater than and less than comparative adjectives (e.g. { "Higher", "Lower" } ) 
--	@param[opt]		{string} start What to start the comparison string by for if you want to increase the bullet level (e.g. "\n***")
--	@returns		{string} Resultant wikitext of comparison string
local function buildCompareString(firstVal, secondVal, valName, digits, unit, compareAdjs, start)
	if (firstVal == nil or secondVal == nil) then return ""	end
	
	local firstValStr = firstVal
	local secondValStr = secondVal
	if (digits ~= nil) then
		firstValStr = Math.round(firstVal, digits)
		secondValStr = Math.round(secondVal, digits)
	end
	if (unit ~= nil) then
		firstValStr = firstValStr..unit
		secondValStr = secondValStr..unit
	end
	local bigWord = compareAdjs ~= nil and compareAdjs[1] or "Higher"
	local smallWord = compareAdjs ~= nil and compareAdjs[2] or "Lower"
	local start = start ~= nil and start or "\n**"
	
	if (firstVal > secondVal) then
		return start.." "..bigWord.." "..valName.." ("..firstValStr.." vs. "..secondValStr..")"
	elseif (secondVal > firstVal) then
		return start.." "..smallWord.." "..valName.." ("..firstValStr.." vs. "..secondValStr..")"
	end
	return ""
end

---	Builds damage comparison string between two attacks.
--	@function		p.buildComparison
--	@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 buildDamageTypeComparisonString(Attack1, Attack2)
	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
	for i, element in ipairs(DAMAGE_TYPES) do
		local damage1 = Attack1.Damage[element] or 0
		local damage2 = Attack2.Damage[element] or 0
		result = result..buildCompareString(damage1, damage2, Tooltip.getFullTooltip(element, 'DamageTypes').." damage", 0.01, nil, {"Higher", "Lower"}, "\n***")
	end
	return result
end

---	Builds comparison list between two gun weapons.
--	@function		p.buildComparison
--	@param			{table} Weapon1 Weapon used for comparison
--	@param			{table} Weapon2 Weapon used to compare the first weapon against
--	@param			{boolean} conclave If true, makes comparison list based on PvP stats, otherwise uses PvE stats; default false
--	@returns		{string} Resultant wikitext of comparison list
local function buildGunComparisonString(Weapon1, Weapon2, Conclave)
	local result = {}
	-- Adding this assignment to support method chaining w/ colon syntax
	result.insert = function(self, elem) table.insert(self, elem) return self end
	
	local Att1 = getAttack(Weapon1, Weapon1['_TooltipAttackDisplay'] or 'Attack1')
	local Att2 = getAttack(Weapon2, Weapon2['_TooltipAttackDisplay'] or 'Attack1')
	if (Conclave) then
		result:insert("* [["..Weapon1.Name.."]], compared to [[Conclave:"..Weapon2.Name.."|"..Weapon2.Name.."]]:")
	else
		result:insert("* [["..Weapon1.Name.."]] ("..(Att1.AttackName or "Normal").."), compared to [["..Weapon2.Name.."]] ("..(Att2.AttackName or "Normal").."):")
	end
	
	local dmgString = ""
	dmgString = dmgString..buildCompareString(p._getValue(Weapon1, "BaseDamage"), p._getValue(Weapon2, "BaseDamage"), "base damage  per projectile", 0.01)
	dmgString = dmgString..buildDamageTypeComparisonString(Att1, Att2)
	if (string.len(dmgString) > 0 and p._getValue(Weapon1, "BaseDamage") == p._getValue(Weapon2, "BaseDamage")) then
		dmgString = "\n**Equal base damage, but different composition:"..dmgString
	end
	dmgString = dmgString..buildCompareString(p._getValue(Weapon1, "TotalDamage"), p._getValue(Weapon2, "TotalDamage"), "total damage", 0.01)
	-- TODO: Find a better way of finding out if an attack is a charge attack; maybe add Trigger = "Charge" to all of those attacks?
	-- result = result..buildCompareString(p._getValue(Weapon1, "ChargeTime", "Attack3"), p._getValue(Weapon2, "ChargeTime", "Attack3"), "charge time", 0.01, " s", {"Slower", "Faster"})
	result:insert(dmgString)
	
	local weapon1Name = p._getValue(Weapon1, "Name")
	local weapon2Name = p._getValue(Weapon2, "Name")
	local isLichWeapon = string.find(weapon1Name, "Kuva") or 
		string.find(weapon1Name, "Tenet") or 
		string.find(weapon2Name, "Kuva") or 
		string.find(weapon2Name, "Tenet")
	
	local progenitorBonusNote = isLichWeapon and " (using max +60% [[Lich System/Progenitor|Progenitor]] bonus if applicable)" or ""
	
	if (not Conclave) then
		result:insert(
			buildCompareString((Att1.CritChance * 100), (Att2.CritChance * 100), "base [[critical chance]]", 0.01, "%")
		):insert(
			buildCompareString(Att1.CritMultiplier, Att2.CritMultiplier, "base [[critical multiplier]]", 0.01, "x")
		):insert(
			buildCompareString((Att1.StatusChance * 100), (Att2.StatusChance * 100), "base [[status chance]]", 0.01, "%")
		):insert(
			buildCompareString(p._getValue(Weapon1, "AvgShotDmg"), p._getValue(Weapon2, "AvgShotDmg"), "[[Damage#Final_Calculations|average damage per shot]]"..progenitorBonusNote, 0.01)
		):insert(
			buildCompareString(p._getValue(Weapon1, "BurstDps"), p._getValue(Weapon2, "BurstDps"), "[[Damage#Final_Calculations|burst DPS]]"..progenitorBonusNote, 0.01)
		):insert(buildCompareString(p._getValue(Weapon1, "SustainedDps"), p._getValue(Weapon2, "SustainedDps"), "[[Damage#Final_Calculations|sustained DPS]]"..progenitorBonusNote, 0.01)
		):insert(
			buildCompareString(p._getValue(Weapon1, "FalloffStart"), p._getValue(Weapon2, "FalloffStart"), "starting [[Damage Falloff|damage falloff]] distance", 0.1, "m", {"Farther", "Closer"})
		):insert(
			buildCompareString(p._getValue(Weapon1, "FalloffEnd"), p._getValue(Weapon2, "FalloffEnd"), "ending damage falloff distance", 0.1, "m", {"Farther", "Closer"})
		):insert(
			buildCompareString(p._getValue(Weapon1, "FalloffReduction"), p._getValue(Weapon2, "FalloffReduction"), "max damage reduction at ending falloff distance", 0.01, "x", {"Greater", "Lesser"})
		)
	end
	
	result:insert(
		buildCompareString(Att1.FireRate, Att2.FireRate, "[[fire rate]]", 0.01, " round(s)/sec")
	):insert(
		buildCompareString(Att1.Multishot or 1, Att2.Multishot or 1, "[[multishot]]", 1, " projectile(s)")
	):insert(
		buildCompareString(Weapon1.Magazine, Weapon2.Magazine, "magazine", 1, " round(s)", {"Larger", "Smaller"})
	):insert(
		buildCompareString(Weapon1.MaxAmmo, Weapon2.MaxAmmo, "max ammo capacity", 1, " round(s)", {"Larger", "Smaller"})
	):insert(
		buildCompareString(Weapon1.Reload, Weapon2.Reload, "[[Reload|reload time]]", 0.01, " s", {"Slower", "Faster"})
	):insert(
		buildCompareString(Weapon1.Spool, Weapon2.Spool, "spool-up", 1, " round(s)", {"Slower", "Faster"})
	):insert(
		buildCompareString(p._getValue(Weapon1, "Accuracy"), p._getValue(Weapon2, "Accuracy"), "[[Accuracy|accurate]]", 0.01, nil, {"More", "Less"})
	):insert(
		buildCompareString(p._getValue(Weapon1, "Polarities"), p._getValue(Weapon2, "Polarities"), "[[Polarity|polarities]]", nil, nil, {"Different", "Different"})
	):insert(
		buildCompareString(Weapon1.Mastery, Weapon2.Mastery, "[[Mastery Rank]] required", 1)
	):insert(
		buildCompareString(Weapon1.Disposition, Weapon2.Disposition, "[[disposition]]", 0.01)
	)
	
	--Handling Syndicate radial effects
	if (Weapon1.SyndicateEffect ~= nil and Weapon2.SyndicateEffect == nil) then
		result:insert("\n** Innate [["..Weapon1.SyndicateEffect.."]] effect")
	elseif (Weapon2.SyndicateEffect ~= nil and Weapon1.SyndicateEffect == nil) then
		result:insert("\n** Lack of an innate [["..Weapon2.SyndicateEffect.."]] effect")
	elseif (Weapon1.SyndicateEffect ~= nil and Weapon2.SyndicateEffect ~= nil and 
			Weapon1.SyndicateEffect ~= Weapon2.SyndicateEffect2) then
		result:insert("\n** Different innate [[Syndicate Radial Effects|Syndicate Effect]]: [["..
			Weapon1.SyndicateEffect.."]] vs. [["..Weapon2.SyndicateEffect.."]]")
	end
	return table.concat(result)
end

---	Builds comparison list between two melee or arch-melee weapons.
--	@function		p.buildComparison
--	@param			{table} Weapon1 Weapon used for comparison
--	@param			{table} Weapon2 Weapon used to compare the first weapon against
--	@param			{boolean} conclave If true, makes comparison list based on PvP stats, otherwise uses PvE stats; default false
--	@returns		{string} Resultant wikitext of comparison list
local function buildMeleeComparisonString(Weapon1, Weapon2, Conclave)
	local result = {}
	-- Adding this assignment to support method chaining w/ colon syntax
	result.insert = function(self, elem) table.insert(self, elem) return self end
	
	local Att1 = getAttack(Weapon1, Weapon1['_TooltipAttackDisplay'] or 'Attack1')
	local Att2 = getAttack(Weapon2, Weapon2['_TooltipAttackDisplay'] or 'Attack1')

	if (Conclave) then
		result:insert("* "..Weapon1.Name..", compared to [[Conclave:"..Weapon2.Name.."|"..Weapon2.Name.."]]:")
	else
		result:insert("* "..Weapon1.Name.." ("..(Att1.AttackName or "Normal").."), compared to [["..Weapon2.Name.."]] ("..(Att2.AttackName or "Normal").."):")
	end
	
	local dmgString = ""
	dmgString = dmgString..buildCompareString(p._getValue(Weapon1, "BaseDamage"), p._getValue(Weapon2, "BaseDamage"), "base damage", 0.01)
	dmgString = dmgString..buildDamageTypeComparisonString(Att1, Att2)
	if (string.len(dmgString) > 0 and p._getValue(Weapon1, "BaseDamage") == p._getValue(Weapon2, "BaseDamage")) then
		dmgString = "\n**Equal base damage, but different composition:"..dmgString	
	end
	result:insert(dmgString)
	
	if (not Conclave) then
		result:insert(
			buildCompareString((Att1.CritChance * 100), (Att2.CritChance * 100), "[[critical chance]]", 0.01, "%")
		):insert(
			buildCompareString(Att1.CritMultiplier, Att2.CritMultiplier, "[[critical multiplier]]", 0.01, "x")
		):insert(
			buildCompareString((Att1.StatusChance * 100), (Att2.StatusChance * 100), "[[status chance]]", 0.01, "%")
		):insert(
			buildCompareString(p._getValue(Weapon1, "MeleeRange"), p._getValue(Weapon2, "MeleeRange"), "Range", 0.01, " m")
		):insert(
			buildCompareString(Weapon1.Disposition, Weapon2.Disposition, "[[disposition]]", 0.01)
		)
	end

	result:insert(
		buildCompareString(Att1.FireRate, Att2.FireRate, "[[attack speed]]", 0.01)
	):insert(
		buildCompareString(p._getValue(Weapon1, "Polarities"), p._getValue(Weapon2, "Polarities"), "[[Polarity|polarities]]", nil, nil, {"Different", "Different"})
	):insert(
		buildCompareString(Weapon1.Mastery, Weapon2.Mastery, "[[Mastery Rank]] required", 1)
	):insert(
		buildCompareString(p._getValue(Weapon1, "ComboDur"), p._getValue(Weapon2, "ComboDur"), "[[Melee Combo|Combo Duration]]", 1, " s")
	):insert(
		buildCompareString(p._getValue(Weapon1, "BlockAngle"), p._getValue(Weapon2, "BlockAngle"), "Block Angle", 1, "&#176;")
	):insert(
		buildCompareString(p._getValue(Weapon1, "StancePolarity"), p._getValue(Weapon2, "StancePolarity"), "[[Stance]] Polarity", nil, nil, {"Different", "Different"})
	)
	return table.concat(result)
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)
	local weaponName1 = frame.args[1]
	local weaponName2 = frame.args[2]

	assert(weaponName1 ~= '' and weaponName2 ~= '', 'p.buildComparison(frame): Must compare two weapons')
	
	local Weapon1 = p._getWeapon(weaponName1)
	local Weapon2 = p._getWeapon(weaponName2)
	
	local comparisonString = ''
	if (Weapon1.Slot == 'Melee' or Weapon1.Slot == 'Arch-Melee') then
		comparisonString = buildMeleeComparisonString(Weapon1, Weapon2)
	else
		comparisonString = buildGunComparisonString(Weapon1, Weapon2)
	end
	return comparisonString..'[[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)
	local weaponName1 = frame.args[1]
	local weaponName2 = frame.args[2]
	
	assert(weaponName1 ~= '' and weaponName2 ~= '', 'p.buildConclaveComparison(frame): Must compare two weapons')
	
	local Weapon1 = p._getWeapon(weaponName1, true)
	local Weapon2 = p._getWeapon(weaponName2, true)
	
	local comparisonString = ''
	if (Weapon1.Slot == 'Melee' or Weapon1.Slot == 'Arch-Melee') then
		comparisonString = buildMeleeComparisonString(Weapon1, Weapon2, true)
	else
		comparisonString = buildGunComparisonString(Weapon1, Weapon2, true)
	end
	return comparisonString..'[[Category:Automatic Comparison]]'
end

-- TODO: Currently unused since we migrated infobox builders to M:Weapons/infobox
---	Adds weapon categories.
--	@function		p.buildAutoboxCategories
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of category links
function p.buildAutoboxCategories(frame)
	local WeapName = frame.args ~= nil and frame.args[1] or frame
	local Weapon = p._getWeapon(WeapName)
	local result = { "[[Category:Automatic Weapon Box]][[Category:Weapons]]" }
	if (Weapon == nil or (Weapon.IgnoreCategories ~= nil and Weapon.IgnoreCategories)) then
		return ""
	end
	table.insert(result, CATEGORY_MAP[Weapon['Class']]..CATEGORY_MAP[Weapon['Trigger']]..CATEGORY_MAP[Weapon['Slot']])
	
	-- Adding appropriate categories to page based on weapon's categorical traits
	for _, trait in pairs(Weapon['Traits'] or {}) do
		table.insert(result, CATEGORY_MAP[trait])
	end
	
	local bestPercent, bestElement = getDamageBias(Weapon[Weapon['_TooltipAttackDisplay'] or 'Attack1'])
	if (bestElement == "Impact" or bestElement == "Puncture" or bestElement == "Slash") then
		if (bestPercent > .38) then
			table.insert(result, "[[Category:"..bestElement.." Damage Weapons]]")
		else
			table.insert(result, "[[Category:Balanced Physical Damage Weapons]]")
		end
	end
	
	for key, value in Table.skpairs(attack.Damage) do
		if (key ~= "Impact" and key ~= "Puncture" and key ~= "Slash") then
			table.insert(result, "[[Category:"..key.." Damage Weapons]]")
		end
	end
	
	return table.concat(result)
end

---	Builds a table that lists out all weapons with a certain damage type being 
--	the highest damage distribution out of all the damage types it deals
--	and the percentage that it makes up of their base damage of the attack specified
--	in their tooltip on the wiki.
--	@function		p.buildDamageTypeTable
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of resultant wikitable
function p.buildDamageTypeTable(frame)
	local damageType = frame.args ~= nil and frame.args[1] or frame
	local Weapons = {}
	local WeapArray = p._getWeapons(function(weaponEntry)
		-- Want to ignore Kitgun entries which have 0 as placeholder damage values
		if (string.find(weaponEntry['Class'], 'Kitgun') ~= nil) then
			return false
		end
		local attackEntry = getAttack(weaponEntry, weaponEntry['_TooltipAttackDisplay'] or 'Attack1')
		assert(attackEntry ~= nil, 'p.buildDamageTypeTable(frame): "'..weaponEntry.Name..
			'" has no attack entry for "Attack1" or attack in "_TooltipAttackDisplay"')
		local dmg, element = getDamageBias(attackEntry)
		return element == damageType
	end)
	
	local result = ''
	local tHeader = string.format([[
{| class = "listtable sortable" style="margin:auto;"
|+ '''Weapons with %s as highest damage distribution'''
|-
! Name !! Slot !! Class !! %s !! data-sort-type="number" | %s%%
]], damageType, Tooltip.getFullTooltip(damageType, 'DamageTypes'), damageType)

	local tRows = {}
	for i, Weapon in pairs(WeapArray) do
		local thisRow = [[
|-
| %s || %s || %s || %s || data-sort-value="%s" | %s]]
		local attack = Weapon['_TooltipAttackDisplay'] or 'Attack1'
		thisRow = string.format(thisRow, 
			Tooltip.getFullTooltip(p._getValue(Weapon, 'Name'), 'Weapons'),
			p._getValue(Weapon, 'Slot'),
			p._getValue(Weapon, 'Class'),
			p._getValue(Weapon, damageType, attack),
			(p._getValue(Weapon, 'DamageBias', attack)),	-- Note that function returns two values for DamageBias, first being the percentage as a decimal
			p._getFormattedValue(Weapon, 'DamageBias', attack)
		)
		table.insert(tRows, thisRow)
	end
	result = tHeader..table.concat(tRows, '\n')..'\n|}'
	return frame:preprocess(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 checkSlotAndMastery = function(x) return x.Slot == weaponSlot and x.Mastery == masteryRank end
	local weapArray = p._getWeapons(checkSlotAndMastery)
	
	local result = {}
	local name = ''
	local shortList = p._shortLinkList(weapArray, true)
	for i, list in Table.skpairs(shortList) do
		table.insert(result, list)
	end
	return frame:preprocess(table.concat(result, ' • '))
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 ranges = { 1550, 1300, 1100, 899, 699, 499 }
	local result = {'{| class="article-table" border="0" cellpadding="1" cellspacing="1" style="width: 100%" \n|-'}
	
	for i = 1, 5 do
		table.insert(result, '\n! scope="col" style="text-align:center;"|'..Icon._Dis(ranges[i]/1000))
	end
	table.insert(result, '\n|-')
	
	for dispoRating = 5, 1, -1 do
		table.insert(result, '\n| style="vertical-align:top; font-size:small" |')
		local checkRightDispo = function(weaponEntry)
			if (weaponEntry['Slot'] ~= nil and weaponEntry['Disposition'] ~= nil) then
				return (weaponSlot == 'All' or weaponEntry['Slot'] == weaponSlot) and
					math.min(math.floor(5 * (weaponEntry['Disposition'] - (weaponEntry['Disposition'] < 1 and 0.3 or 0.309))), 5) == dispoRating
			end
			return false
		end
		-- Filtering out weapons that are between certain disposition ranges
		local weapArray = p._getWeapons(checkRightDispo)
		
		-- Building a list of weapons with a particular disposition rating (e.g. 5 dots)
		local weaponDispoList = {}
		
		-- Want to iterate in descending order so highest disposition weapon is near the top of table
		local descendingOrder = function(a, b)
			-- a and b are number indexes
			return weapArray[a]['Disposition'] > weapArray[b]['Disposition']
		end
		for _, weaponEntry in Table.skpairs(weapArray, descendingOrder) do
			table.insert(weaponDispoList, '\n* [['..weaponEntry['Name']..']] ('..weaponEntry['Disposition']..')')
		end
		
		table.insert(result, table.concat(weaponDispoList))
	end
	table.insert(result, '\n|}')
	
	return table.concat(result)
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]
	local weapArray = p._getWeapons(function(x) return x.Slot == weaponSlot and x.Conclave end)
	
	local result = {}
	local shortList = p._shortLinkList(weapArray, false)
	for i, list in Table.skpairs(shortList) do
		table.insert(result, list)
	end
	-- Need to concat extra '*' for listing first element
	return '*'..table.concat(result, '\n* ')
end

---	Inserts weapon into appropriate subtable
--	@function		insert
--	@param			{table} t Table to insert into
--	@param			{string} key What subtable to insert into
--	@param			{string} name Weapon name
--	@param			{number} val Weapon stat value
--	@returns		{table} t New table
local function insertWeapon(t, key, name, val)
	local exist = false
	
	for _, v in ipairs(t[key]) do
		if v[1] == val then
			table.insert(v[2], name)
			exist = true
			break
		end
	end
	if not exist then
		table.insert(t[key], {val, {name}})
	end
	
	return t
end

---	Gets the top percentile and top 3 stats of a specified category of weapons.
--	@function		getWeaponStats
--	@param			{string} slot Weapon slot (primary, secondary, etc.)
--	@param			{string} classes Weapon class
--	@param			{string} triggers Weapon trigger type
--	@param			{number} att Weapon attack number as stored in <code>/data</code> (1 for "Attack1", 2 for "Attack2", etc.)
--	@param			{number} max Maximum number of Attack keys
--	@returns		{table} vals3 Table of percentiles
--	@returns		{table} top3 Table of top 3 weapons for each stat
--	@returns		{number} count Number of weapons that fit specified cat
--	@returns		{table} Weapons Weapons that fit specified cat
local function getWeaponStats(slot, classes, triggers, att, max)
	local attacks = { 'Normal Attack', 'Secondary Attack', 'Charge Attack', 
		'Charge Area Attack', 'Area Attack', 'Secondary Charge Attack', 
		'Secondary Area Attack', 'Charged Throw Attack', 'Throw Attack' }
	local count, temp, i, specific = 0, 0, 0
	local vals, vals2, vals3, top3, stats
	local Weapons = {}
	
	if slot == 'Melee' or slot == 'Arch-Melee' or slot == 'Arch-Gun' then
		vals = { Damage = {}, CritChance = {}, CritMultiplier = {},
			StatusChance = {}, FireRate = {}, MeleeRange = {} }
		vals2 = { Damage = {}, CritChance = {}, CritMultiplier = {},
			StatusChance = {}, FireRate = {}, MeleeRange = {} }
		vals3 = { Damage = {}, CritChance = {}, CritMultiplier = {},
			StatusChance = {}, FireRate = {}, MeleeRange = {} }
		top3 = { Damage = {}, CritChance = {}, CritMultiplier = {},
			StatusChance = {}, FireRate = {}, MeleeRange = {} }
		stats = { 'Damage', 'CritChance', 'CritMultiplier',
			'StatusChance', 'FireRate', 'MeleeRange' }
	else
		vals = { Damage = {}, Multishot = {}, CritChance = {}, CritMultiplier = {},
			StatusChance = {}, FireRate = {}, Magazine = {}, MaxAmmo = {}, Reload = {} }
		vals2 = { Damage = {}, Multishot = {}, CritChance = {}, CritMultiplier = {},
			StatusChance = {}, FireRate = {}, Magazine = {}, MaxAmmo = {}, Reload = {} }
		vals3 = { Damage = {}, Multishot = {}, CritChance = {}, CritMultiplier = {},
			StatusChance = {}, FireRate = {}, Magazine = {}, MaxAmmo = {}, Reload = {} }
		top3 = { Damage = {}, Multishot = {}, CritChance = {}, CritMultiplier = {},
			StatusChance = {}, FireRate = {}, Magazine = {}, MaxAmmo = {}, Reload = {} }
		stats = { 'Damage', 'Multishot', 'CritChance', 'CritMultiplier',
			'StatusChance', 'FireRate', 'Magazine', 'MaxAmmo', 'Reload' }
	end
	
	for name, weapon in pairs(WeapData) do
		specific = not weapon.Class:find('Kitgun')
		if classes then specific = Table.contains(classes, weapon.Class) end
		if triggers then specific = specific and Table.contains(triggers, weapon.Trigger) end
		if not(weapon._IgnoreEntry) and weapon.Slot == slot and specific then
			if slot == 'Arch-Melee' then mw.log(name) end
			table.insert(Weapons, {Name = name, Family = weapon.Family or name, Image = weapon.Image or "Panel.png"})
			if weapon['Attack'..att] and not((weapon['Attack'..att].AttackName or attacks[att]):lower():find('aoe')) then
				count = count + 1
				for _, stat in ipairs(stats) do
					temp = 0
					if stat == 'Damage' then
						for _, damage in pairs(weapon['Attack'..att].Damage) do
							temp = temp + damage
						end
						temp = temp * (weapon['Attack'..att].Multishot or weapon.Multishot or 1)
					else
						temp = weapon['Attack'..att][stat] or weapon[stat] or 1
					end
					if stat == 'Reload' then temp = 1 / temp end
					top3 = insertWeapon(top3, stat, name, temp)
					
					i = '"'..temp..'"'
					if not vals[stat][i] then vals[stat][i] = 0 end
					vals[stat][i] = vals[stat][i] + 1
				end
			end
			if att == max then WeapData[name] = nil end
		end
	end
	
	for _, stat in ipairs(stats) do
		for val, c in pairs(vals[stat]) do
			i = val:gsub('"', '')
			table.insert(vals2[stat], { tonumber(i), c / count })
		end
		table.sort(vals2[stat], function(a, b) return a[1] < b[1] end)
		
		for i, val in ipairs(vals2[stat]) do
			if i > 1 then val[2] = val[2] + vals2[stat][i - 1][2] end
		end
		table.sort(vals2[stat], function(a, b) return a[1] > b[1] end)
	
		i = 0
		-- 0.9, 0.775, 0.65, 0.525, 0.4, 0.275, 0.15
		for j, val in ipairs(vals2[stat]) do
			if 0.90 - val[2] - i/8 > 0 then table.insert(vals3[stat], vals2[stat][j - 1][1]); i = i + 1 end
			if i > 6 then break; end
		end
		
		i = #vals3[stat]
		for j = 1, math.ceil((7 - i) / 2) do
			table.insert(vals3[stat], 1, "math.huge")
		end
		for j = 1, math.floor((7 - i) / 2) do
			table.insert(vals3[stat], "-math.huge")
		end
	end
	vals = nil
	vals2 = nil
	
	for _, v in pairs(top3) do
		table.sort(v, function(a, b) return a[1] > b[1] end)
		for i = #v, 4, -1 do
			table.remove(v, i)
		end
	end
	if top3.Reload and top3.Reload[1] then top3.Reload[1][1] = 1 / top3.Reload[1][1] end
	if top3.Reload and top3.Reload[2] then top3.Reload[2][1] = 1 / top3.Reload[2][1] end
	if top3.Reload and top3.Reload[3] then top3.Reload[3][1] = 1 / top3.Reload[3][1] end
	
	return vals3, top3, count, Weapons
end

---	Dumps a preprocessed table of weapon stats and info to copy into M:Weapons/ppdata.
--	Invoked on [[Module:Weapons/ppdata/doc]].
--	@function		p.ppData
--	@param			{table} frame Frame object
--	@returns		{string} ppdata preprocessed wikitext string in Lua table formatting
function p.ppData(frame)
	local subData = {
		Primary = {
			{	Type = 'Arm Cannons',
				Classes = {'Arm Cannon'},
				Triggers = {
					{	Name = 'Auto',
						{'Auto'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Assault Rifles',
				Classes = {'Rifle'},
				Triggers = {
					{	Name = 'Active',
						{'Active'},
						Weapons = {},
					},
					{	Name = 'Auto',
						{'Auto'},
						Weapons = {},
					},
					{	Name = 'Auto-Burst',
						{'Auto Burst'},
						Weapons = {},
					},
					{	Name = 'Auto-Spool',
						{'Auto-Spool'},
						Weapons = {},
					},
					{	Name = 'Burst',
						{'Burst'},
						Weapons = {},
					},
					{	Name = 'Charge',
						{'Charge'},
						Weapons = {},
					},
					{	Name = 'Held',
						{'Held'},
						Weapons = {},
					},
					{	Name = 'Hybrid',
						{'Auto / Semi', 'Burst / Semi', 'Burst / Semi / Auto', 'Auto / Charge'},
						Weapons = {},
					},
					{	Name = 'Semi-Auto',
						{'Semi-Auto'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Bows',
				Classes = {'Bow', 'Crossbow', 'Exalted Weapon'},
				Triggers = {
					{	Name = 'Auto',
						{'Auto'},
						Weapons = {},
					},
					{	Name = 'Charge',
						{'Charge'},
						Weapons = {},
					},
					{	Name = 'Hybrid',
						{'Semi / Burst'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Launchers',
				Classes = {'Launcher'},
				Triggers = {
					{	Name = 'Active',
						{'Active'},
						Weapons = {},
					},
					{	Name = 'Auto',
						{'Auto'},
						Weapons = {},
					},
					{	Name = 'Charge',
						{'Charge'},
						Weapons = {},
					},
					{	Name = 'Semi-Auto',
						{'Semi-Auto'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Shotguns',
				Classes = {'Shotgun'},
				Triggers = {
					{	Name = 'Auto',
						{'Auto'},
						Weapons = {},
					},
					{	Name = 'Auto-Spool',
						{'Auto-Spool'},
						Weapons = {},
					},
					{	Name = 'Charge',
						{'Charge'},
						Weapons = {},
					},
					{	Name = 'Duplex',
						{'Duplex'},
						Weapons = {},
					},
					{	Name = 'Held',
						{'Held'},
						Weapons = {},
					},
					{	Name = 'Hybrid',
						{'Auto / Semi'},
						Weapons = {},
					},
					{	Name = 'Semi-Auto',
						{'Semi-Auto'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Sniper Rifles',
				Classes = {'Sniper Rifle'},
				Triggers = {
					{	Name = 'Charge',
						{'Charge'},
						Weapons = {},
					},
					{	Name = 'Semi-Auto',
						{'Semi-Auto'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Spearguns',
				Classes = {'Speargun'},
				Triggers = {
					{	Name = 'Auto',
						{'Auto'},
						Weapons = {},
					},
					{	Name = 'Charge',
						{'Auto Charge', 'Charge'},
						Weapons = {},
					},
				},
			},
		},

		Secondary = {
			{	Type = 'Crossbows',
				Classes = {'Crossbow'},
				Triggers = {
					{	Name = 'Hybrid',
						{'Burst / Charge'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Dual Pistols',
				Classes = {'Dual Pistols', 'Exalted Weapon'},
				Triggers = {
					{	Name = 'Auto',
						{'Auto'},
						Weapons = {},
					},
					{	Name = 'Auto-Spool',
						{'Auto-Spool'},
						Weapons = {},
					},
					{	Name = 'Burst',
						{'Burst'},
						Weapons = {},
					},
					{	Name = 'Charge',
						{'Charge'},
						Weapons = {},
					},
					{	Name = 'Semi-Auto',
						{'Semi-Auto'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Dual Shotguns',
				Classes = {'Dual Shotguns'},
				Triggers = {
					{	Name = 'Auto-Spool',
						{'Auto-Spool'},
						Weapons = {},
					},
					{	Name = 'Semi-Auto',
						{'Semi-Auto'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Pistols',
				Classes = {'Pistol', 'Exalted Weapon'},
				Triggers = {
					{	Name = 'Auto',
						{'Auto'},
						Weapons = {},
					},
					{	Name = 'Burst',
						{'Burst'},
						Weapons = {},
					},
					{	Name = 'Charge',
						{'Charge'},
						Weapons = {},
					},
					{	Name = 'Duplex',
						{'Duplex'},
						Weapons = {},
					},
					{	Name = 'Held',
						{'Held'},
						Weapons = {},
					},
					{	Name = 'Hybrid',
						{'Auto / Burst', 'Semi / Charge'},
						Weapons = {},
					},
					{	Name = 'Semi-Auto',
						{'Semi-Auto'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Shotgun Sidearms',
				Classes = {'Shotgun Sidearm'},
				Triggers = {
					{	Name = 'Auto',
						{'Auto'},
						Weapons = {},
					},
					{	Name = 'Auto-Spool',
						{'Auto-Spool'},
						Weapons = {},
					},
					{	Name = 'Semi-Auto',
						{'Semi-Auto'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Thrown',
				Classes = {'Thrown'},
				Triggers = {
					{	Name = 'Active',
						{'Active'},
						Weapons = {},
					},
					{	Name = 'Auto',
						{'Auto'},
						Weapons = {},
					},
					{	Name = 'Hybrid',
						{'Auto / Semi'},
						Weapons = {},
					},
				},
			},
		},

		Melee = {
			{	Type = 'Brawler',
				Classes = {
					{	Name = 'Claws',
						{'Claws'},
						Weapons = {},
					},
					{	Name = 'Fist',
						{'Fist'},
						Weapons = {},
					},
					{	Name = 'Sparring',
						{'Sparring'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Daggers',
				Classes = {
					{	Name = 'Dagger',
						{'Dagger'},
						Weapons = {},
					},
					{	Name = 'Dual Daggers',
						{'Dual Daggers'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Heavies',
				Classes = {
					{	Name = 'Heavy Blade',
						{'Heavy Blade'},
						Weapons = {},
					},
					{	Name = 'Hammer',
						{'Hammer'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Ranged',
				Classes = {
					{	Name = 'Blade and Whip',
						{'Blade and Whip'},
						Weapons = {},
					},
					{	Name = 'Glaive',
						{'Glaive'},
						Weapons = {},
					},
					{	Name = 'Gunblade',
						{'Gunblade'},
						Weapons = {},
					},
					{	Name = 'Whip',
						{'Whip'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Staves',
				Classes = {
					{	Name = 'Nunchaku',
						{'Nunchaku'},
						Weapons = {},
					},
					{	Name = 'Polearm',
						{'Polearm'},
						Weapons = {},
					},
					{	Name = 'Scythe',
						{'Scythe'},
						Weapons = {},
					},
					{	Name = 'Staff',
						{'Staff'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Swords',
				Classes = {
					{	Name = 'Dual Swords',
						{'Dual Swords'},
						Weapons = {},
					},
					{	Name = 'Machete',
						{'Machete'},
						Weapons = {},
					},
					{	Name = 'Nikana',
						{'Nikana'},
						Weapons = {},
					},
					{	Name = 'Rapier',
						{'Rapier'},
						Weapons = {},
					},
					{	Name = 'Sword',
						{'Sword'},
						Weapons = {},
					},
					{	Name = 'Sword and Shield',
						{'Sword and Shield'},
						Weapons = {},
					},
					{	Name = 'Two-Handed Nikana',
						{'Two-Handed Nikana'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Miscellaneous',
				Classes = {
					{	Name = 'Assault Saw',
						{'Assault Saw'},
						Weapons = {},
					},
					{	Name = 'Exalted Weapon',
						{'Exalted Weapon'},
						Weapons = {},
					},
					{	Name = 'Tonfa',
						{'Tonfa'},
						Weapons = {},
					},
					{	Name = 'Warfan',
						{'Warfan'},
						Weapons = {},
					},
				},
			},
		},

		Archwing = {
			{	Type = 'Archwing Weapons',
				Classes = {
					{	Name = 'Arch-Gun',
						{'Arch-Gun'},
						Weapons = {},
					},
					{	Name = 'Arch-Melee',
						{'Arch-Melee'},
						Weapons = {},
					},
					{	Name = 'Exalted Weapon',
						{'Exalted Weapon'},
						Weapons = {},
					},
				},
			},
		},

		Robotic = {
			{	Type = 'Assault Rifles',
				Classes = {'Rifle'},
				Triggers = {
					{	Name = 'Auto',
						{'Auto'},
						Weapons = {},
					},
					{	Name = 'Auto-Spool',
						{'Auto-Spool'},
						Weapons = {},
					},
					{	Name = 'Burst',
						{'Burst'},
						Weapons = {},
					},
					{	Name = 'Charge',
						{'Charge'},
						Weapons = {},
					},
					{	Name = 'Held',
						{'Held'},
						Weapons = {},
					},
					{	Name = 'Semi-Auto',
						{'Semi-Auto'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Shotguns',
				Classes = {'Shotgun'},
				Triggers = {
					{	Name = 'Auto',
						{'Auto'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Sniper Rifles',
				Classes = {'Sniper Rifle'},
				Triggers = {
					{	Name = 'Auto',
						{'Auto'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Pistols',
				Classes = {'Pistol'},
				Triggers = {
					{	Name = 'Burst',
						{'Burst'},
						Weapons = {},
					},
				},
			},
			{	Type = 'Melee',
				Classes = {'Glaive'},
				Triggers = {
					{	Name = 'Auto',
						{'Auto'},
						Weapons = {},
					},
				},
			},
		},
	}
	local WEAPON_SLOTS = {
		{'Primary', 'Primary'},
		{'Secondary', 'Secondary'},
		{'Melee', 'Melee'},
		{'Arch-Melee', 'Archwing'},
		{'Arch-Gun', 'Archwing'},
		{'Robotic', 'Robotic'},
	}
	local str = {'<pre>'}
	local percentiles, top3, count, data
	local breakLoops = false
	
	WeapData = Table.deepCopy(WeaponData.Weapons)
	for _, weapSlot in ipairs(WEAPON_SLOTS) do
		if weapSlot[1] == 'Melee' or weapSlot[1] == 'Arch-Melee' then
			percentiles, top3, count = getWeaponStats(weapSlot[1], nil, nil, 1, 2)
			if count >= 3 then
				subData[weapSlot[2]].Attack1 = {}
				subData[weapSlot[2]].Attack1.Percentiles = percentiles
				subData[weapSlot[2]].Attack1.Top3 = top3
			end
			for i, v in ipairs(subData[weapSlot[2]]) do
				for j, classes in ipairs(v.Classes) do
					mw.log()
					percentiles, top3, count, weaps = getWeaponStats(weapSlot[1], classes[1], nil, 1, 1)
					mw.logObject(weaps)
					if count >= 3 then
						subData[weapSlot[2]][i].Classes[j].Attack1 = {}
						subData[weapSlot[2]][i].Classes[j].Attack1.Percentiles = percentiles
						subData[weapSlot[2]][i].Classes[j].Attack1.Top3 = top3
					end
					classes.Weapons = weaps
				end
			end
		elseif weapSlot[1] == 'Arch-Gun' then
			for i = 1, 7 do
				percentiles, top3, count = getWeaponStats(weapSlot[1], nil, nil, i, 8)
				if count >= 3 then
					subData[weapSlot[2]]['Attack'..i] = {}
					subData[weapSlot[2]]['Attack'..i].Percentiles = percentiles
					subData[weapSlot[2]]['Attack'..i].Top3 = top3
				end
			end
			for i, v in ipairs(subData[weapSlot[2]]) do
				for j, classes in ipairs(v.Classes) do
					for k = 1, 7 do
						percentiles, top3, count, weaps = getWeaponStats(weapSlot[1], classes[1], nil, k, 7)
						if count >= 3 then
							subData[weapSlot[2]][i].Classes[j]['Attack'..k] = {}
							subData[weapSlot[2]][i].Classes[j]['Attack'..k].Percentiles = percentiles
							subData[weapSlot[2]][i].Classes[j]['Attack'..k].Top3 = top3
						end
					end
					table.insert(weaps, {Name = 'Arquebex', Family = 'Arquebex', Image = 'ArquebexFixed.png'})
					classes.Weapons = weaps
				end
			end
		else
			for i = 1, 7 do
				percentiles, top3, count = getWeaponStats(weapSlot[1], nil, nil, i, 8)
				if count >= 3 then
					subData[weapSlot[2]]['Attack'..i] = {}
					subData[weapSlot[2]]['Attack'..i].Percentiles = percentiles
					subData[weapSlot[2]]['Attack'..i].Top3 = top3
				end
			end
			for i, v in ipairs(subData[weapSlot[2]]) do
				for j, triggers in ipairs(v.Triggers) do
					for k = 1, 7 do
						percentiles, top3, count, weaps = getWeaponStats(weapSlot[1], v.Classes, triggers[1], k, 7)
						if count >= 3 then
							subData[weapSlot[2]][i].Triggers[j]['Attack'..k] = {}
							subData[weapSlot[2]][i].Triggers[j]['Attack'..k].Percentiles = percentiles
							subData[weapSlot[2]][i].Triggers[j]['Attack'..k].Top3 = top3
						end
					end
					triggers.Weapons = weaps
				end
			end
		end
	end
	
	data = mw.dumpObject(subData):gsub('table#%d+ %{', '%{'):gsub('%["', '')
	data = data:gsub('"%]', ''):gsub('-nan', 'nil'):gsub('  ', '	'):gsub('inf', 'math.huge')
	data = data:gsub('"math.huge"', 'math.huge'):gsub('"%-math.huge"', '%-math.huge')
	table.insert(str, 'return '..data..'\n')
	table.insert(str, '</pre>')
	
	return frame:preprocess(table.concat(str, '\n'))
end

return p