(Query and formatting to find containers with a specific item (with the calling template Template:Cargo container item)) |
(Corrected the way some probability figures were presented) |
||
Line 176: | Line 176: | ||
local wrapper = mw.html.create("div") |
local wrapper = mw.html.create("div") |
||
:addClass("mw-collapsible mw-collapsed wikitable") |
:addClass("mw-collapsible mw-collapsed wikitable") |
||
− | :cssText("padding:10px; border-width: 1px; border-style: solid") |
+ | :cssText("padding:10px; border-width: 1px; border-style: solid; display: flow-root") |
:wikitext("Containers with loot containing <b>[["..(results.item.page or results.item.name).."|"..results.item.name.."]]</b>") |
:wikitext("Containers with loot containing <b>[["..(results.item.page or results.item.name).."|"..results.item.name.."]]</b>") |
||
:tag("div") |
:tag("div") |
||
Line 397: | Line 397: | ||
-- Same occurance count, create array for results |
-- Same occurance count, create array for results |
||
if (occuranceCount == stats.bestContainersByProbabilityOccurances) then |
if (occuranceCount == stats.bestContainersByProbabilityOccurances) then |
||
− | table.insert(stats.bestContainersByProbability, i) |
+ | table.insert(stats.bestContainersByProbability, containers[i].guid) |
else |
else |
||
− | stats.bestContainersByProbability = { i } |
+ | stats.bestContainersByProbability = { containers[i].guid } |
end |
end |
||
Line 410: | Line 410: | ||
-- Same quantity, create array for results |
-- Same quantity, create array for results |
||
if (bestQty == stats.bestContainersByQuantityQty) then |
if (bestQty == stats.bestContainersByQuantityQty) then |
||
− | table.insert(stats.bestContainersByQuantity, i) |
+ | table.insert(stats.bestContainersByQuantity, containers[i].guid) |
else |
else |
||
− | stats.bestContainersByQuantity = { i } |
+ | stats.bestContainersByQuantity = { containers[i].guid } |
end |
end |
||
Line 421: | Line 421: | ||
end |
end |
||
+ | if (#containers > 0) then |
||
− | + | for day = 1, 20 do |
|
− | if (stats.dayTotals[day] >= stats.bestDayQty) then |
+ | if ((stats.dayTotals[day] or 0) >= stats.bestDayQty) then |
− | + | ||
− | -- Same quantity, add to array for results |
+ | -- Same quantity, add to array for results |
⚫ | |||
− | + | if (stats.dayTotals[day] == stats.bestDayQty) then |
|
⚫ | |||
− | else |
+ | else |
− | stats.bestDays = { day } |
+ | stats.bestDays = { day } |
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
end |
end |
||
− | + | if ((stats.dayTotals[day] or 0) <= stats.worstDayQty) then |
|
− | + | ||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
− | if (stats.dayTotals[day] <= stats.worstDayQty) then |
||
− | + | else |
|
+ | stats.worstDays = { day } |
||
⚫ | |||
⚫ | |||
− | if (stats.dayTotals[day] == stats.worstDayQty) then |
||
+ | |||
⚫ | |||
+ | stats.worstDayQty = stats.dayTotals[day] |
||
⚫ | |||
⚫ | |||
end |
end |
||
⚫ | |||
⚫ | |||
end |
end |
||
end |
end |
||
Line 473: | Line 475: | ||
end |
end |
||
− | function |
+ | function findContainerByGuid(guid) |
⚫ | |||
+ | if (results.containers[i].guid == guid) then |
||
⚫ | |||
+ | end |
||
+ | end |
||
+ | |||
+ | return nil |
||
+ | end |
||
+ | |||
+ | function convertGuidsToLinks(guids) |
||
local tbl = {} |
local tbl = {} |
||
+ | if (guids == nil or #guids == 0) then return nil end |
||
⚫ | |||
+ | for i = 1, #guids do |
||
⚫ | |||
+ | local container = findContainerByGuid(guids[i]) |
||
tbl[i] = "[["..container.page.."#"..container.guid.."|"..container.name.."]]" |
tbl[i] = "[["..container.page.."#"..container.guid.."|"..container.name.."]]" |
||
end |
end |
||
Line 483: | Line 496: | ||
local tbl = mw.html.create("table") |
local tbl = mw.html.create("table") |
||
− | :addClass("wikitable") |
+ | :addClass("wikitable mw-collapsible mw-collapsed") |
:tag("tr") |
:tag("tr") |
||
:tag("th") |
:tag("th") |
||
Line 566: | Line 579: | ||
:done() |
:done() |
||
:tag("td") |
:tag("td") |
||
− | :node((#stats.bestContainersByProbability > 5) and "-" or table.concat( |
+ | :node((#stats.bestContainersByProbability > 5) and "-" or table.concat(convertGuidsToLinks(stats.bestContainersByProbability), ", ").." ("..stats.bestContainersByProbabilityOccurances.." occurances)") |
:done() |
:done() |
||
:done() |
:done() |
||
Line 578: | Line 591: | ||
:done() |
:done() |
||
:tag("td") |
:tag("td") |
||
− | :node((#stats.bestContainersByQuantity > 5) and "-" or table.concat( |
+ | :node((#stats.bestContainersByQuantity > 5) and "-" or table.concat(convertGuidsToLinks(stats.bestContainersByQuantity), ", ").." ("..stats.bestContainersByQuantityQty..")") |
:done() |
:done() |
||
:done() |
:done() |
Revision as of 14:32, 12 October 2021
This module depends on: Module:Arguments,Module:Common and Module:Yesno, with data stored in Module:Lootlist/data and Module:Lootlist/statedata. These scripts are written in Lua.
A complex module used to support the multiple lootlist templates.
Uses the following data modules:
- Module:Lootlist/data, loot lists and item data to avoid excessive cargo queries.
- Module:Lootlist/statedata, used for validating Xorshift states in debug mode.
See also
The above documentation is transcluded from Module:Lootlist/doc. (edit | history)
-- Module:Lootlist
-- Author: Macklin
-- Description: Used to generate/evaluate random lootlists and provides formatting
-- support for Template:Container, Template:ContanerList, and Template:ContainerTable
local p = {}
local cargo = mw.ext.cargo
local bit32 = require("bit32")
local common = require("Module:Common")
local data = nil
-- Make some commonly-used functions local to this module
local getArgs = require("Module:Arguments").getArgs
local isNilOrEmpty = common.isNilOrEmpty
local stringEndsWith = common.endsWith
-- Container functions
-- Entry point from Template:Container
-- 1. Calculates loot for each of the days and constructs a cargo store with the
-- parameters passed to the original template. Although we could just store the
-- results directly on the page, this saves a lot of space in comparison
-- 2. Creates and outputs what is actually shown on the page
-- Optional parameters:
-- cargo_store - If "yes" is passed, this container will be stored in cargo
-- display - Should be "box" for a ContainerList, "row" for a ContainerTable, otherwise defaults to "none"
function p.Container(frame)
local args = getArgs(frame)
-- Fetch the parameters that were originally passed to the template containing the #invoke to this function
-- We can't modify the frame args since they're read only, but we can make a copy of it
local container = { }
for k, v in pairs(args) do
container[k] = v
end
-- Do checks to see if some required parameters are missing
if common.tableValueIsNilOrEmpty(container, "name") then return
elseif common.tableValueIsNilOrEmpty(container, "position_x") then return
elseif common.tableValueIsNilOrEmpty(container, "position_z") then return
end
-- Fetch the root lootlist with this name from cargo
local lootlist = GetLootlist(container["lootlist"])
-- Not every container uses a lootlist. If one isn't present (i.e. no items or nil table) then skip generating the loot
if (lootlist ~= nil and lootlist.items ~= nil) then
-- These are two of the three parameters needed for seed generation, the last being the day of the month
-- A fourth parameter, the player GameObject name's hashcode, is the same regardless and is defined in the InitSeed function
local posx = tonumber(container["position_x"])
local posz = tonumber(container["position_z"])
-- Add a day_x_loot and day_x_loot_qty field for each day
for day = 1, 20 do
-- Initialize the Xorshift state for this container on this day
local seed = CalculateSeedForContainer(posx, posz, day)
Xorshift128_InitSeed(seed)
-- Evaluate the lootlist
local loot = EvaluateLootList(lootlist)
-- Stack the items
loot = StackItems(loot)
-- Sort the items
SortItems(loot)
-- Add name, page, filters
AddItemData(loot)
container["day_"..day.."_loot"] = loot;
end
end
-- Process fixed loot
if (not isNilOrEmpty(container["inventory"])) then
-- Create item objects by splitting the inventory and inventory_qty fields
local inventory = { }
local fixedInventoryIds = mw.text.split(container["inventory"], ";", true)
local fixedInventoryQty = mw.text.split(container["inventory_qty"], ";", true)
for i = 1, #fixedInventoryIds do
inventory[i] = { }
inventory[i].id = fixedInventoryIds[i]
inventory[i].count = fixedInventoryQty[i]
end
-- Sort the items
SortItems(inventory)
-- Add name, page, filters
AddItemData(inventory)
-- Store in container for use in other functions
container["inventory_items"] = inventory;
end
if (args["cargo_store"] == "yes") then
ContainerCargoStore(frame, container)
end
local display = args["display"] or "none"
if (display == "box") then
return BuildContainerListBox(frame, container)
elseif (display == "row") then
return BuildContainerTableRow(frame, container)
else
return
end
end
-- Performs a #cargo_store to store the Container in the associated cargo table. Does not return a value
-- Args should be a table containing all the fields passed to the original Container template, plus the additional day_x_loot tables
function ContainerCargoStore(frame, container)
-- Because callParserFunction can only take string arguments, we remove all non-string fields from the container
-- The day_x_loot fields need to be converted from tables into two separate "List (;) of String"'s before actually doing the cargo_store
local storeArgs = { }
for k, v in pairs(container) do
local day = string.match(k, "day_(%d+)_loot.*")
-- For all day_x_loot tables
if (day ~= nil and #v > 0) then
local lootStr = ""
local lootQtyStr = ""
-- Build up the day_x_loot string for day x
-- Doing string concatenation with so few strings is in this case faster than constructing a table and doing table.concat
for i, item in ipairs(v) do
lootStr = lootStr..item.id..";"
lootQtyStr = lootQtyStr..item.count..";"
end
storeArgs["day_"..day.."_loot"] = lootStr:sub(1, -2)
storeArgs["day_"..day.."_loot_qty"] = lootQtyStr:sub(1, -2)
-- For all other values (strings only)
elseif (type(v) == "string") then
storeArgs[k] = v
end
end
storeArgs["_table"] = "Container_poe1"
-- Call cargo store parser function
frame:callParserFunction("#cargo_store:", storeArgs)
end
-- Entry point from Cargo container item.
function p.CargoContainerItem(frame)
local args = getArgs(frame)
if (isNilOrEmpty(args["item"])) then
error("Parameter \"item\" must be set")
return
end
-- Perform query
local results = CargoContainerItemQuery(args["item"])
-- Build HTML tables
local queryTable = BuildContainerItemQueryTable(results.containers)
local statTable = BuildContainerItemQueryStatisticsTable(results)
-- Wrap table in collapsible
local wrapper = mw.html.create("div")
:addClass("mw-collapsible mw-collapsed wikitable")
:cssText("padding:10px; border-width: 1px; border-style: solid; display: flow-root")
:wikitext("Containers with loot containing <b>[["..(results.item.page or results.item.name).."|"..results.item.name.."]]</b>")
:tag("div")
:addClass("mw-collapsible-content")
:cssText("overflow:hidden")
:node(queryTable..statTable)
:done()
return tostring(wrapper)
end
-- Finds every container whose random or fixed item set contains a specific item
-- Takes a single argument, "item", the ID of the item to return results on
-- Returns:
-- "item" - an object containing the itemData
-- "containers" - an array with the fields name, page, location, guid, and quantity - an array where each element is the number of the queried item that appears in that container on that day. Element 0 in quantity will be fixed loot, while element 20 is the quantity for day 20
-- "stats" - a set of interesting statistics
function CargoContainerItemQuery(item)
-- Force to lower for comparison
item = string.lower(item)
local itemData = GetItemData(item)
-- First, sanity check that the item actually exists in the item data
if (itemData == nil) then
error("No item with the ID "..item.." exists in the data module")
return
end
local containers = {}
-- Perform a separate query for each day (and "inventory", fixed days). This is nessecary because using multiple HOLDS breaks when there are multiple results on the same page (returning only the last result on a page). Additionally, using 20 LEFT OUTER JOINS in a single query is extremely slow, and often times out
-- If a day contains multiple entries of the same item, a separate result for each entry will be returned
for day = 0, 20 do
-- 0 index is inventory, 1+ are days
local name = (day == 0) and "inventory" or "day_"..day.."_loot"
-- Construct the query
local tables = "Container_poe1, Container_poe1__"..name..", Container_poe1__"..name.."_qty"
local fields = "Container_poe1._ID=id, Container_poe1.name=name, Container_poe1._pageName=page, Container_poe1.location=location, Container_poe1.guid=guid, Container_poe1__"..name.."._value="..name..", Container_poe1__"..name.."_qty._value="..name.."_qty"
local args = {}
args.join = "Container_poe1._ID = Container_poe1__"..name.."._rowID, Container_poe1._ID = Container_poe1__"..name.."_qty._rowID"
args.where = "Container_poe1__"..name.."._value = \""..item.."\" AND Container_poe1__"..name.."._position = Container_poe1__"..name.."_qty._position"
args.limit = 0
-- Perform the query
local results = cargo.query(tables, fields, args)
-- Process the results
for r = 1, #results do
-- Get the quantity
local qty = tonumber(results[r][name.."_qty"])
-- Nil supporting data
results[r][name] = nil
results[r][name.."_qty"] = nil
-- Get existing data in output based on GUID (hopefully no containers have the same GUID)
local data = common.tableGetTableWithKeyOfValue(containers, "guid", results[r].guid, true)
-- If there is no existing data, create it
if (data == nil) then
data = mw.clone(results[r])
-- An index-keyed table where each index is a quantity of the item on this day
data.quantity = {}
table.insert(containers, data)
end
-- Increment instead of just set, because there may be containers where a day contains multiple entries for the same item (this is certainly the case for some inventories)
data.quantity[day] = data.quantity[day] ~= nil and (data.quantity[day] + qty) or qty
end
--[[
-- HOLDS version of the above query section, is a little bit slower and doesn't select individual elements in the List (;) of String
-- Construct the query
local tables = "Container_poe1"
local fields = "name, _pageName=page, location, guid, day_"..i.."_loot=day_"..i.."_loot, day_"..i.."_loot_qty=day_"..i.."_loot_qty"
local args = {}
args.where = "Container_poe1.day_"..i.."_loot HOLDS \""..item.."\""
args.limit = 0
-- Perform the query
local results = cargo.query(tables, fields, args)
--]]
end
-- Get some stats about the loot
local stats = ContainerItemQueryStatistics(containers)
return { item = itemData, containers = containers, stats = stats }
end
-- Returns a collection of interesting and/or useless statistics about the container query
function ContainerItemQueryStatistics(containers)
local stats = {}
stats.labels = {}
-- The number of containers containing this item
stats.numContainers = #containers
stats.numContainersTotal = 0
stats.numContainersPerc = 0
-- The total amount of occurances of this item, an occurance being an instance on a day where this item will be found in a container. If a container has fixed loot containing this item, the container has 20 occurances
stats.cumulativeOccurances = 0
-- The total amount of items that can be obtained for all days combined, including fixed loot (this is impossible though, since containers can't be looted more than once)
stats.cumulativeQty = 0
-- By selecting the best day for each container, this is the most amount of this item that can obtained. This includes fixed loot
stats.maximalQty = 0
stats.maximalOccurances = 0
-- By selecting the worst day for each container, this is the most amount of this item that can be obtained. This includes days with 0, and includes fixed loot
-- This could also be seen as the guaranteed quantity.
stats.minimalQty = 0
stats.minimalOccurances = 0
-- The best container for looting this item, this container has the most days where this item appears, fixed loot counts as one day
stats.bestContainersByProbability = { }
stats.bestContainersByProbabilityOccurances = 0
-- The best container for looting this item, this container has the most amount of this item on a single day, including fixed loot
stats.bestContainersByQuantity = { }
stats.bestContainersByQuantityQty = 0
-- An array containing the total amount of items looted from all containers on that day
stats.dayTotals = {}
-- The best days of the month for looting this item, the days with the highest total number of this item that will spawn
stats.bestDays = { }
-- Tracks the actual quantity of the above
stats.bestDayQty = -math.huge
-- The worst days of the month for looting this item, the days with the lowest total number of this item that will spawn
stats.worstDays = { }
-- Tracks the actual quantity of the above
stats.worstDayQty = math.huge
-- The percentage of loot outcomes that contain this item over all other outcomes. Every container has 20 outcomes representing each day
stats.probability = 0
for i = 1, #containers do
local bestDay = 0
local bestDayQty = 0 -- <- Not including fixed
local worstDay = 0
local worstDayQty = 0 -- <- Not including fixed
local occuranceCount = 0
local occuranceCountInclFixed = 0
local occursAllDays = true -- <- Not including fixed
local fixedLootQty = (containers[i].quantity[0] or 0)
for day = 0, 20 do
local qty = containers[i].quantity[day] or 0
local qtyInclFixed = qty + fixedLootQty
stats.cumulativeQty = stats.cumulativeQty + qtyInclFixed
stats.dayTotals[day] = (stats.dayTotals[day] or 0) + qty
if (day > 0) then
-- Track day with the highest quantity
if (qty > bestDayQty) then
bestDay = day
bestDayQty = qty
end
-- Track day with the lowest quantity
if (qty < worstDayQty) then
worstDay = day
worstDayQty = qty
end
if (qty == 0) then
-- Invalidate occursAllDays
occursAllDays = false
end
-- Increment occurance count
if (qty > 0) then
occuranceCount = occuranceCount + 1
end
if (qty + fixedLootQty > 0) then
occuranceCountInclFixed = occuranceCountInclFixed + 1
end
end
end
local bestQty = (bestDayQty + fixedLootQty)
-- For this container, add the best day and fixed quantity
stats.maximalQty = stats.maximalQty + bestDayQty + fixedLootQty
stats.minimalQty = stats.minimalQty + (occursAllDays == true and worstDayQty or 0) + fixedLootQty
stats.cumulativeOccurances = stats.cumulativeOccurances + occuranceCountInclFixed
stats.maximalOccurances = stats.maximalOccurances + 1
stats.minimalOccurances = stats.minimalOccurances + ((occursAllDays == true or fixedLootQty > 0) and 1 or 0)
if (occuranceCount >= stats.bestContainersByProbabilityOccurances) then
-- Same occurance count, create array for results
if (occuranceCount == stats.bestContainersByProbabilityOccurances) then
table.insert(stats.bestContainersByProbability, containers[i].guid)
else
stats.bestContainersByProbability = { containers[i].guid }
end
stats.bestContainersByProbabilityOccurances = occuranceCount
end
if (bestQty >= stats.bestContainersByQuantityQty) then
-- Same quantity, create array for results
if (bestQty == stats.bestContainersByQuantityQty) then
table.insert(stats.bestContainersByQuantity, containers[i].guid)
else
stats.bestContainersByQuantity = { containers[i].guid }
end
stats.bestContainersByQuantityQty = bestQty
end
end
if (#containers > 0) then
for day = 1, 20 do
if ((stats.dayTotals[day] or 0) >= stats.bestDayQty) then
-- Same quantity, add to array for results
if (stats.dayTotals[day] == stats.bestDayQty) then
table.insert(stats.bestDays, day)
else
stats.bestDays = { day }
end
stats.bestDayQty = stats.dayTotals[day]
end
if ((stats.dayTotals[day] or 0) <= stats.worstDayQty) then
-- Same quantity, add to array for results
if (stats.dayTotals[day] == stats.worstDayQty) then
table.insert(stats.worstDays, day)
else
stats.worstDays = { day }
end
stats.worstDayQty = stats.dayTotals[day]
end
end
end
local args = { limit = 0 }
local results = cargo.query("Container_poe1", "COUNT(1)=count", args )
stats.numContainersTotal = tonumber(results[1].count)
stats.numContainersPerc = (stats.numContainers / stats.numContainersTotal) * 100
stats.numContainersPerc = common.roundToDecimalPlaces(stats.numContainersPerc, 2)
-- Determine the total number of outcomes by multiplying the number of containers by 20 (20 different outcomes for each day)
stats.probability = (stats.cumulativeOccurances / (stats.numContainersTotal * 20)) * 100
stats.probability = common.roundToDecimalPlaces(stats.probability, 2)
return stats
end
function BuildContainerItemQueryStatisticsTable(results)
local stats = results.stats
function convertAllToOrdinal(days)
local tbl = {}
for i = 1, #days do
tbl[i] = common.stringAddOrdinal(days[i])
end
return tbl
end
function findContainerByGuid(guid)
for i = 1, #results.containers do
if (results.containers[i].guid == guid) then
return results.containers[i]
end
end
return nil
end
function convertGuidsToLinks(guids)
local tbl = {}
if (guids == nil or #guids == 0) then return nil end
for i = 1, #guids do
local container = findContainerByGuid(guids[i])
tbl[i] = "[["..container.page.."#"..container.guid.."|"..container.name.."]]"
end
return tbl
end
local tbl = mw.html.create("table")
:addClass("wikitable mw-collapsible mw-collapsed")
:tag("tr")
:tag("th")
:attr("colspan", "2")
:css("text-align", "left")
:node("Item statistics")
:done()
:done()
:tag("tr")
:tag("td")
:node("Number of containers")
:done()
:tag("td"):node(stats.numContainers.." of "..stats.numContainersTotal.." ("..stats.numContainersPerc.."%)")
:done()
:tag("tr")
:tag("td")
:tag("span")
:addClass("tooltip")
:attr("title", "The percentage of loot list evaluations where this item occurs over all other outcomes across every container. Keep in mind that every container has exactly 20 outcomes representing each day. A container with fixed loot counts as 20 occurances of this item")
:node("Item probability")
:done()
:done()
:tag("td")
:node(stats.probability.."% ("..stats.cumulativeOccurances.." of "..(stats.numContainersTotal * 20).." possible outcomes)")
:done()
:done()
:tag("tr")
:tag("td")
:tag("span")
:addClass("tooltip")
:attr("title", "By selecting the best day for each container, this is the most amount of this item that can obtained after looting all containers. This includes fixed loot")
:node("Maximum available items")
:done()
:done()
:tag("td")
:node(stats.maximalQty.." (in "..stats.maximalOccurances.." occurances)")
:done()
:done()
:tag("tr")
:tag("td")
:tag("span")
:addClass("tooltip")
:attr("title", "By selecting the worst day for each container (including days with 0, always including fixed loot), this is the most amount of this item that can obtained after looting all containers. Also can be seen as the amount of guaranteed items.")
:node("Minimum available items")
:done()
:done()
:tag("td")
:node(stats.minimalQty.." (in "..stats.minimalOccurances.." occurances)")
:done()
:done()
:tag("tr")
:tag("td")
:tag("span")
:addClass("tooltip")
:attr("title", "The best days of the month for looting this item, i.e. the days with the highest number of this item that will spawn")
:node("Best day(s)")
:done()
:done()
:tag("td")
:node((#stats.bestDays > 5) and "-" or table.concat(convertAllToOrdinal(stats.bestDays), ", ").." ("..stats.bestDayQty..")")
:done()
:done()
:tag("tr")
:tag("td")
:tag("span")
:addClass("tooltip")
:attr("title", "The worst days of the month for looting this item, i.e. the days with the lowest number of this item that will spawn")
:node("Worst day(s)")
:done()
:done()
:tag("td")
:node((#stats.worstDays > 5) and "-" or table.concat(convertAllToOrdinal(stats.worstDays), ", ").." ("..stats.worstDayQty..")")
:done()
:done()
:tag("tr")
:tag("td")
:tag("span")
:addClass("tooltip")
:attr("title", "The containers with the highest probability of spawning the item. These containers have the most days where this item appears, ignoring fixed loot")
:node("Best container(s) by probability")
:done()
:done()
:tag("td")
:node((#stats.bestContainersByProbability > 5) and "-" or table.concat(convertGuidsToLinks(stats.bestContainersByProbability), ", ").." ("..stats.bestContainersByProbabilityOccurances.." occurances)")
:done()
:done()
:tag("tr")
:tag("td")
:tag("span")
:addClass("tooltip")
:attr("title", "The containers containing the most amount of this item on a single day, including fixed loot.")
:node("Best container(s) by quantity")
:done()
:done()
:tag("td")
:node((#stats.bestContainersByQuantity > 5) and "-" or table.concat(convertGuidsToLinks(stats.bestContainersByQuantity), ", ").." ("..stats.bestContainersByQuantityQty..")")
:done()
:done()
:done()
return tostring(tbl)
end
-- Because cargo queries seem to be very expensive (in some cases adding tens of
-- seconds to the time it takes to evaluate loot lists for an area), cargo queries
-- have been replaced with simpler/faster lookups in an auxiliary data module which
-- gets fully loaded once for each page containing an #invoke to this module.
-- This replaces all queries to Lootlist_poe1, and queries to get filter strings
-- The only down-side is that the data module is static and will not update dynamically.
-- New items or changes to the associated fields will not be reflected in the table,
-- and will have to be manually regenerated by copying the output of this function
-- to Module:Lootlist/data
-- Depite performance being secondary to poe1 (since they screwed up (?) the RNG system
-- by making it depend on things that never change), it will be required for poe2
-- where the RNG depends on the player name - therefore we have to generate lists on
-- the fly via a JS invoke, and make the whole process take as little time as possible
function p.GenerateLootlistDataModule()
-- First generate lootlist/lootitem data
-- Get all lootitems. These will be sorted by lootlist then index/position
local query = cargo.query("Lootitem_poe1", "lootlist, position, item, is_lootlist, count, weight, always", { orderBy = "lootlist, position", limit = 10000 })
local lootLists = {}
-- Process the query items
for _, item in ipairs(query) do
local lootListStr = item["lootlist"];
local lootList = common.tableGetTableWithKeyOfValue(lootLists, "id", lootListStr, false)
-- Lootitems only have an internalname to identify them, since this is the only field we can rely on
-- The page/item name is also in cargo, but we ignore this since they might not be accurate
local newItem = { }
newItem.id = common.ternary(common.isNilOrEmpty(item["item"]), nil, item["item"])
newItem.isLootlist = common.ternary(item["is_lootlist"] ~= nil and item["is_lootlist"] == "1", true, false)
newItem.count = item["count"]
newItem.weight = item["weight"]
newItem.always = common.ternary(item["always"] ~= nil and item["always"] == "1", true, false)
-- Create new lootlist entry if one doesn't exist
if (lootList == nil) then
lootList =
{
id = lootListStr,
items = { },
totalWeight = 0,
}
table.insert(lootLists, lootList);
end
if (newItem.always == false) then
lootList.totalWeight = lootList.totalWeight + newItem.weight
end
-- Add this item to the lootlist items (keeps in sequence)
table.insert(lootList.items, tonumber(item["position"]) + 1, newItem)
end
-- Next generate item data, used to convert internalname to name, page
-- also generate filter strings while we're at it
local items = { }
local args = { orderBy = "name", limit = 10000 }
local queries =
{
{ tbl = "Item_poe1", fields = "internalname, _pageName = page, name, LOWER(item_category) = category, LOWER(item_type) = type, is_unique" },
{ tbl = "Shield_poe1", fields = "internalname, _pageName = page, name, CONCAT('shield') = category, LOWER(shield_type) = type, is_unique" },
{ tbl = "Armor_poe1", fields = "internalname, _pageName = page, name, CONCAT('armor') = category, LOWER(armor_type) = type, is_unique" },
{ tbl = "Weapon_poe1", fields = "internalname, _pageName = page, name, CONCAT('weapon') = category, LOWER(weapon_type) = type, is_unique" }
}
-- Perform a single query for each table retrieving all rows
for q = 1, #queries do
local results = cargo.query(queries[q].tbl, queries[q].fields, args)
-- Add all items from the results
for _, item in pairs(results) do
if (item.internalname == nil) then
error("No internalname found in "..queries[q].tbl.." for "..item.page.."!")
end
if (items[item.internalname] ~= nil) then
error("Duplicate internalnames found in "..queries[q].tbl.." for "..item.internalname.." - "..items[item.internalname].page.." and "..item.page)
end
-- Names and pages are likely not escaped, so we have to do so manually
item.name = string.format("%q", item.name)
if (item.page ~= nil) then
item.page = string.format("%q", item.page)
end
-- Create the filter string
local filters = { item.category, item.type }
if (item.is_unique == "yes") then table.insert(filters, "unique_item") end
item.filters = table.concat(filters, ";")
-- Instead of creating a new table for the item, we just modify the existing table in the query to suit
-- nil'ing values that we no longer need
item.category = nil
item.type = nil
item.is_unique = nil
-- Only include the name, unless the page and name are different
if (item.name == item.page) then
item.page = nil
end
-- Force internalname lower
local id = item.internalname:lower()
item.internalname = nil
-- Some items have multiple internalnames, which are typically separated by a <br/>
-- For these, split into multiple separate items
if (string.find(id, ".*<br/>.*") ~= nil) then
local ids = mw.text.split(id, "<br/>")
-- Add the item multiple times at each key
for i = 1, #ids do
ids[i] = mw.text.trim(ids[i]) -- Trim whitespace at ends of string
items[ids[i]] = item
end
-- Only one internalname present
else
items[id] = item
end
end
end
-- Buffer output
local buffer = { "local data =\n{\n" }
-- Loot lists
table.insert(buffer, "\tlootlists = \n\t{\n")
for _, list in ipairs(lootLists) do
table.insert(buffer, "\t\t[\""..list.id.."\"] =\n\t\t{\n")
table.insert(buffer, "\t\t\ttotal_weight = "..list.totalWeight..",\n")
table.insert(buffer, "\t\t\titems =\n\t\t\t{\n")
for _, item in ipairs(list.items) do
table.insert(buffer, "\t\t\t\t{ ")
if (item.id == nil) then table.insert(buffer, "id = nil, ")
else table.insert(buffer, "id = \""..item.id.."\", ") end
table.insert(buffer, "is_lootlist = "..tostring(item.isLootlist)..", ")
table.insert(buffer, "weight = "..item.weight..", ")
table.insert(buffer, "count = "..item.count..", ")
table.insert(buffer, "always = "..tostring(item.always).." },\n")
end
table.insert(buffer, "\t\t\t}\n\t\t},\n")
end
table.insert(buffer, "\t},\n")
-- Items
table.insert(buffer, "\titems = \n\t{\n")
for key, item in orderedPairs(items) do
table.insert(buffer, "\t\t[\""..key.."\"] = { ")
table.insert(buffer, "name = "..item.name.." , ")
if (item.page ~= nil) then
table.insert(buffer, "page = "..item.page..", ")
end
table.insert(buffer, "filters = \""..item.filters.."\" },\n")
end
table.insert(buffer, "\t},\n")
table.insert(buffer, "}")
return table.concat(buffer)
end
-- OUTPUT --
-- This section is for functions that output wikitext to be used on the page
-- Constructs the "row" wikitext used within the much more simple ContainerTable
function BuildContainerTableRow(frame, container)
local iconGrid = BuildContainerIconGrid(container)
local tooltip = BuildContainerTooltip(frame, container, iconGrid, "row")
local filterStr = BuildContainerFilterString(container)
local root = mw.html.create("tr")
:attr("id", container["guid"])
-- Details cell (name + icon grid)
:tag("td")
:cssText("vertical-align:top")
:tag("div"):cssText("display:flex; flex-direction:column; height:100%; min-width:165px")
:wikitext("<b>"..container["name"].."</b>"..tooltip)
:tag("div"):cssText("margin-top:auto; font-size:9px; color:grey; text-align:right;")
:wikitext(container["internalname"] or "")
:done()
:done()
:done()
-- Image cell
:tag("td"):cssText("padding:0")
:wikitext(container["image"] and "[[File:"..container["image"].."|200px]]")
:done()
-- Group + description cell
:tag("td")
:cssText("vertical-align:top")
:wikitext(container["description"] or "")
:done()
-- Fixed items cell
local fixedItems = root:tag("td"):cssText("vertical-align:top; font-size:smaller")
if (container["inventory_items"] ~= nil) then
local fixedInventoryItems = container["inventory_items"]
local fixedItemsList = fixedItems:tag("ul")
-- Loop over items, adding a li element for each one
for i = 1, #fixedInventoryItems do
-- Create list item
local li = mw.html.create("li")
:addClass("loot-container-fixed-item")
:attr("data-filters", fixedInventoryItems[i].filters)
-- Only include quantity if it's > 1
if (tonumber(fixedInventoryItems[i].count) > 1) then
li:wikitext(fixedInventoryItems[i].count, "x ")
end
-- Add link
if (fixedInventoryItems[i].page ~= nil) then
li:wikitext("[[", fixedInventoryItems[i].page, "|", fixedInventoryItems[i].name, "]]")
else
li:wikitext("[[", fixedInventoryItems[i].name, "]]")
end
-- Add list item to list
fixedItemsList:node(li)
end
end
-- Random items cells
if (not isNilOrEmpty(container["lootlist"])) then
-- Loop over all 20 day_x_loot tables
for i = 1, 20 do
local loot = container["day_"..i.."_loot"]
-- Create a cell containing all the items on a specific day
local dayLoot = root:tag("td")
:addClass("lootlist-results-day")
:cssText("vertical-align:top; font-size:smaller")
:attr("data-day", tostring(i))
:tag("ul")
-- Create a list item for each loot item
for _, item in ipairs(loot) do
local li = mw.html.create("li"):addClass("lootlist-results-item")
-- Add filter strings on an item level
if (not isNilOrEmpty(item.filters)) then
li:attr("data-filters", item.filters)
end
-- Quantity - Only include if > 1
if (item.count > 1) then
li:wikitext(item.count, "x ")
end
-- Link (add link label if page name exists)
if (item.page ~= nil) then
li:wikitext("[[", item.page, "|", item.name, "]]")
else
li:wikitext("[[", item.name, "]]")
end
-- Add list item to list
dayLoot:node(li)
end
end
-- If this container doesn't contain random loot, create a cell spanning 20 columns
else
root:tag("td"):attr("colspan", "20")
end
return tostring(root)
end
-- Construct the "box" wikitext to be used in Template:ContainerList
function BuildContainerListBox(frame, container)
local iconRow = BuildContainerIconRow(container)
local tooltip = BuildContainerTooltip(frame, container, iconRow, "box")
local filterStr = BuildContainerFilterString(container)
local header = "<b>"..container["name"].."</b>"
--[[
if (not isNilOrEmpty(container["description"])) then
header = header.."<br/>"..container["description"]
end
]]--
local mapNodeString = ""
if (not isNilOrEmpty(container["mapnode"])) then
mapNodeString = container["mapnode"].." ↲"
end
-- Begin creating loot container
local tbl = mw.html.create("div")
:addClass("loot-container")
:attr("id", container["internalname"])
:attr("data-name", container["name"])
:attr("data-group", container["grouping"])
:attr("data-filters", filterStr)
--:cssText("position:relative; font-size:smaller; width:100%; height:100%; box-sizing:border-box; border:1px solid #7E5900; border-radius:7px; overflow:hidden")
-- Header "row"
:tag("div")
:addClass("loot-container-header")
--:cssText("text-align:center; line-height:22px; border-bottom:1px solid #7E5900;")
:wikitext(header)
:tag("div")
:addClass("loot-container-header-arrow")
--:cssText("position:absolute; left:6px; top:0px; cursor:pointer; transform:rotate(-90deg); transition:transform 0.5s ease 0s;")
:wikitext("▼")
:done()
:tag("div")
:addClass("loot-container-header-mapnode")
--:cssText("position:absolute; right:6px; top:0px; cursor:pointer; font-weight:bold")
:wikitext(mapNodeString)
:done()
:done()
-- Detail / icon "row"
:wikitext(tooltip)
local items = tbl:tag("div"):cssText("padding:0.2em 0.4em;")
-- Add fixed items row
if (not isNilOrEmpty(container["inventory"]) and not isNilOrEmpty(container["inventory_qty"])) then
local fixedInventoryItems = container["inventory_items"]
local fixedItems = items:wikitext("Fixed loot:"):tag("ul")
-- Loop over items, adding a li element for each one
for i = 1, #fixedInventoryItems do
-- Create list item
local li = mw.html.create("li")
:addClass("loot-container-fixed-item")
:attr("data-filters", fixedInventoryItems[i].filters)
-- Only include quantity if it's > 1
if (tonumber(fixedInventoryItems[i].count) > 1) then
li:wikitext(fixedInventoryItems[i].count, "x ")
end
-- Add link
if (fixedInventoryItems[i].page ~= nil) then
li:wikitext("[[", fixedInventoryItems[i].page, "|", fixedInventoryItems[i].name, "]]")
else
li:wikitext("[[", fixedInventoryItems[i].name, "]]")
end
-- Add list item to list
fixedItems:node(li)
end
end
-- Add random items row
if (not isNilOrEmpty(container["lootlist"])) then
local containerID = ""
if (container["guid"] ~= nil) then
containerID = string.sub(container["guid"], 1, 8)
elseif (container["internalname"] ~= nil) then
containerID = container["internalname"]
else
containerID = tostring(math.random(0, 1000000))
end
-- Create row, cell, and open div
-- The dropdown is added in JavaScript
local randomItems = items:wikitext("Random loot:<sup>[[Random loot|?]]</sup>")
:tag("div"):addClass("lootlist-results"):attr("id", "lootlist-results-"..containerID):css("column-count", "2")
-- Loop over all 20 day_x_loot tables
for i = 1, 20 do
local loot = container["day_"..i.."_loot"]
-- Create colitem/ul containing all the items on a specific day
local dayLoot = randomItems:tag("div")
:addClass("colitem")
:addClass("lootlist-results-day")
:attr("data-day", tostring(i))
:tag("span")
:wikitext(common.stringAddOrdinal(i), ":")
:done():tag("ul")
-- Create a list item for each loot item
for _, item in ipairs(loot) do
local li = mw.html.create("li"):addClass("lootlist-results-item")
-- Add filter strings on an item level
if (not isNilOrEmpty(item.filters)) then
li:attr("data-filters", item.filters)
end
-- Quantity - Only include if > 1
if (item.count > 1) then
li:wikitext(item.count, "x ")
end
-- Link (add link label if page name exists)
if (item.page ~= nil) then
li:wikitext("[[", item.page, "|", item.name, "]]")
else
li:wikitext("[[", item.name, "]]")
end
-- Add list item to list
dayLoot:node(li)
end
end
end
return tostring(tbl)
end
-- Build the table used to output results of a Cargo container item query
function BuildContainerItemQueryTable(containers)
function compareByLocation(a, b)
return a.location < b.location
end
-- Sort the containers by location
table.sort(containers, compareByLocation)
-- Start building the HTML
local tbl = mw.html.create("table")
tbl:addClass("wikitable sortable")
tbl:css("text-align", "center")
local highestQty = 0
-- Get the highest quantity
-- Used to determine whether to use a dot (if all 1)
-- Used to determine the column width
for i = 1, #containers do
for day = 0, 20 do
if (containers[i].quantity[day] ~= nil and
containers[i].quantity[day] > highestQty) then
highestQty = containers[i].quantity[day]
break
end
end
end
local allSingleQty = (highestQty == 1)
local qtyColumnWidth = 3.32
-- Table header row
local head = tbl:tag("tr")
head:node("<th style=\"min-width:230px\" rowspan=\"2\">Location</th>")
head:node("<th rowspan=\"2\">Container</th>")
head:node("<th style=\"min-width:"..tostring(qtyColumnWidth).."em\" rowspan=\"2\">Fixed</th>")
head:node("<th colspan=\"20\" style=\"text-align:left\">Random loot (amount on each day of month)<sup>[[Random loot|?]]</sup></th>")
head = tbl:tag("tr")
for i = 1, 20 do
head:tag("th")
:css("min-width", tostring(qtyColumnWidth).."em")
:wikitext(tostring(i))
end
local lastLocation
-- Build container rows
for i = 1, #containers do
-- Start of container row
local row = tbl:tag("tr")
-- If this container is in a new location to the previous row start a new rowspan
if (containers[i].location ~= lastLocation) then
lastLocation = containers[i].location
local rowspan = nil
-- Count the number of other containers in this location
for j = i + 1, #containers do
if (containers[j].location == lastLocation) then
if (rowspan == nil) then rowspan = 1 end
rowspan = rowspan + 1
else
-- We can ignore containers after since they won't be the same location
-- (the containers were sorted by location earlier, and are contigious)
break
end
end
-- Location cell
row:tag("td")
:attr("rowspan", rowspan)
:css("text-align", "left")
:wikitext("[["..containers[i].location.."]]")
end
-- Container cell, link to Loot page
row:tag("td")
:cssText("white-space:nowrap; text-align:left")
:wikitext("[["..containers[i].page.."#"..containers[i].guid.."|"..containers[i].name.."]]")
-- Finally, the loot cells
for day = 0, 20 do
local text = containers[i].quantity[day] ~= nil and (allSingleQty == true and "[[File:Point-icon.png|link=]]" or tostring(containers[i].quantity[day])) or nil
row:tag("td"):wikitext(text)
end
end
return tostring(tbl)
end
-- Build up the icon row of the container
function BuildContainerIconRow(container)
local buffer = { "" }
-- Container contains fixed loot
if (not isNilOrEmpty(container["inventory"])) then
table.insert(buffer, "📌 ")
end
if (not isNilOrEmpty(container["lootlist"])) then
table.insert(buffer, "🎲 ([[Loot lists#")
table.insert(buffer, container["lootlist"])
table.insert(buffer, "|")
table.insert(buffer, container["lootlist"])
table.insert(buffer, "]]) ")
end
-- Locked
if (container["locked"] == "yes") then
table.insert(buffer, "[[File:CUR_locked_noarrow.png|x20px|middle|Locked|link=]] (")
local keyLink = nil;
-- Key present
if (not isNilOrEmpty(container["key_item"])) then
keyLink = "[["..container["key_item"].."|Key]]"
end
-- Key required
if (container["key_required"] == "yes") then
table.insert(buffer, keyLink)
-- Key not required, or not present
else
table.insert(buffer, container["lock_difficulty"])
if (keyLink ~= nil) then
table.insert(buffer, ", ")
table.insert(buffer, keyLink)
end
end
table.insert(buffer, ") ")
end
-- Trapped
if (container["trapped"] == "yes" and not isNilOrEmpty(container["trap_difficulty"])) then
table.insert(buffer, "[[File:CUR_disarm_noarrow.png|x20px|middle|Trapped|link=]] (")
table.insert(buffer, container["trap_difficulty"])
table.insert(buffer, ") ")
end
-- Hidden
if (container["hidden"] == "yes" and not isNilOrEmpty(container["detect_difficulty"])) then
table.insert(buffer, "[[File:CUR_noLOS_noarrow.png|x20px|middle|Hidden|link=]] (")
table.insert(buffer, container["detect_difficulty"])
table.insert(buffer, ") ")
end
-- Stealing
if (container["steal_faction"] ~= nil and string.lower(container["steal_rep"]) ~= "none") then
table.insert(buffer, "[[File:CUR_steal_noarrow.png|x20px|middle|Stealing|link=]] (<i>")
table.insert(buffer, container["steal_faction"])
table.insert(buffer, "</i>)")
end
return table.concat(buffer)
end
-- Build up the icon grid of the container
function BuildContainerIconGrid(container)
local grid = mw.html.create("div")
:cssText("display:grid; grid-template-columns:24px auto; grid-auto-rows:minmax(2em, min-content); align-items:center; font-size:smaller")
-- Contains fixed loot
if (not isNilOrEmpty(container["inventory"])) then
grid:tag("div"):wikitext("📌"):cssText("font-size:16px; line-height:16px; align-self:center")
grid:tag("div"):wikitext("Fixed loot")
end
-- Contains random loot
if (not isNilOrEmpty(container["lootlist"])) then
grid:tag("div"):wikitext("🎲"):cssText("font-size:16px; line-height:16px; align-self:center")
grid:tag("div"):wikitext("Random loot<br/>([[Loot lists#"..container["lootlist"].."|"..container["lootlist"].."]])")
end
-- Locked
if (container["locked"] == "yes") then
local buffer = { "Locked ("}
local keyLink = nil;
-- Key present
if (not isNilOrEmpty(container["key_item"])) then
keyLink = "[["..container["key_item"].."]]"
end
-- Key required (i.e. cannot open with Mechanics)
if (container["key_required"] == "yes" and keyLink ~= nil) then
table.insert(buffer, keyLink)
-- Key not required, or not present
else
table.insert(buffer, "Difficulty ")
table.insert(buffer, container["lock_difficulty"])
if (keyLink ~= nil) then
table.insert(buffer, ", ")
table.insert(buffer, keyLink)
end
end
table.insert(buffer, ") ")
grid:tag("div"):wikitext("[[File:CUR_locked_noarrow.png|20px|middle|Locked|link=]]")
:cssText("align-self:center")
grid:tag("div"):wikitext(table.concat(buffer))
end
-- Trapped
if (container["trapped"] == "yes" and not isNilOrEmpty(container["trap_difficulty"])) then
grid:tag("div"):wikitext("[[File:CUR_disarm_noarrow.png|x20px|middle|Trapped|link=]]")
:cssText("align-self:center")
grid:tag("div"):wikitext("Trapped (Difficulty "..container["trap_difficulty"]..")")
end
-- Hidden
if (container["hidden"] == "yes" and not isNilOrEmpty(container["detect_difficulty"])) then
grid:tag("div"):wikitext("[[File:CUR_noLOS_noarrow.png|x20px|middle|Hidden|link=]]")
:cssText("align-self:center")
grid:tag("div"):wikitext("Hidden (Difficulty "..container["detect_difficulty"]..")")
end
-- Stealing
if (container["steal_faction"] ~= nil and string.lower(container["steal_rep"]) ~= "none") then
grid:tag("div"):wikitext("[[File:CUR_steal_noarrow.png|x20px|middle|Stealing|link=]]")
grid:tag("div"):wikitext("Stealing (<i>"..container["steal_faction"].."</i>)")
end
return tostring(grid)
end
-- Build up the tooltip in its entirety, mostly building the content string
-- We use a table to concatenate strings here instead of using .. to save us a fair bit of CPU time and memory.
-- It may not be as pretty, but it's tested to be much faster with this many strings
function BuildContainerTooltip(frame, container, wrapper, display)
local buffer = { "" }
-- Container contains fixed loot
if (not isNilOrEmpty(container["inventory"])) then
table.insert(buffer, "<span style=\"width:24px; display:inline-block\">📌</span>Contains <b>fixed loot</b> always present on opening\n")
end
if (not isNilOrEmpty(container["lootlist"])) then
table.insert(buffer, "<span style=\"width:24px; display:inline-block\">🎲</span>Contains <b>random loot</b> — One of the following 20 sets of items is present depending on the day of the month\n")
end
table.insert(buffer, "<hr style=\"margin-bottom:10px\"/>\r\n")
-- Container is locked
if (container["locked"] == "yes") then
local lockDifficulty = tonumber(container["lock_difficulty"])
local lockKey = container["key_item"]
local isKeyRequired = container["key_required"] == "yes"
table.insert(buffer, "[[File:CUR locked noarrow.png|x16px|middle]] Container is <b>locked</b>")
-- Key not required, list mechanics requirements
if (isKeyRequired == false) then
table.insert(buffer, " (Difficulty ")
table.insert(buffer, tostring(lockDifficulty))
table.insert(buffer, ")\n* Requires one of the following:\n** ")
table.insert(buffer, tostring(lockDifficulty - 1))
table.insert(buffer, " [[Mechanics]], 3 Lockpicks\n** ")
table.insert(buffer, tostring(lockDifficulty))
table.insert(buffer, " [[Mechanics]], 1 Lockpick\n** ")
table.insert(buffer, tostring(lockDifficulty + 1))
table.insert(buffer, " [[Mechanics]]\n")
-- Key for this container is present, but not required
if (not isNilOrEmpty(lockKey)) then
table.insert(buffer, "* Can also be opened with the [[")
table.insert(buffer, tostring(lockKey))
table.insert(buffer, "]]\n")
end
end
-- Key required
if (isKeyRequired == true and lockKey ~= nil) then
table.insert(buffer, "\n* Requires the [[")
table.insert(buffer, lockKey)
table.insert(buffer, "]] (cannot be lockpicked)\n")
end
table.insert(buffer, "\n")
end
-- Container is trapped
if (container["trapped"] == "yes" and not isNilOrEmpty(container["trap_difficulty"])) then
local trapItem = container["trap_item"]
table.insert(buffer, "[[File:CUR disarm noarrow.png|x16px|sub]] Container is <b>trapped</b> (Difficulty ")
table.insert(buffer, container["trap_difficulty"])
table.insert(buffer, ")<br/>\n* Requires at least [[Mechanics]] ")
table.insert(buffer, container["trap_difficulty"])
table.insert(buffer, " to disable")
if (container["trap_item"] ~= nil) then
table.insert(buffer, "\n* Disabling gives a [[")
table.insert(buffer, container["trap_item"])
table.insert(buffer, "]]")
end
table.insert(buffer, "\n")
end
-- Container is hidden
if (container["hidden"] == "yes" and container["detect_difficulty"] ~= nil) then
local detectDifficulty = tonumber(container["detect_difficulty"])
-- Trap is hidden
table.insert(buffer, "[[File:CUR noLOS noarrow.png|x16px|sub]] ")
table.insert(buffer, common.ternary(container["trapped"] == "yes" and container["trap_difficulty"] ~= nil, "Trap", "Container"))
table.insert(buffer, " is <b>hidden</b> (Difficulty ")
table.insert(buffer, detectDifficulty)
table.insert(buffer, ")<br/>\n* Requires at least [[Mechanics]] ")
table.insert(buffer, detectDifficulty)
table.insert(buffer, " (in Scouting Mode) to be detected at a 1 meter distance")
table.insert(buffer, "\n")
end
-- Container is owned
if (not isNilOrEmpty(container["steal_faction"])) then
table.insert(buffer, "[[File:CUR steal noarrow.png|x16px|sub]] Container is <b>owned</b> by [[")
table.insert(buffer, container["steal_faction"])
table.insert(buffer, "]]\n* Being caught stealing incurs a ")
local rep = string.lower(container["steal_rep"])
if (rep == "veryminor") then rep = "Slight (1)"
elseif (rep == "minor") then rep = "Minor (2)"
elseif (rep == "average") then rep = "Moderate (4)"
elseif (rep == "major") then rep = "Major (6)"
elseif (rep == "verymajor") then rep = "Extraordinary (8)"
end
table.insert(buffer, rep)
table.insert(buffer, " reputation loss with this faction\n")
if (container["attack_thief"] == "yes") then
table.insert(buffer, "\n* Nearby faction members become hostile")
end
if (container["ally_attack_thief"] == "yes") then
table.insert(buffer, "\n* Nearby allied faction members become hostile")
end
end
-- Add technical information
local technical = { }
if (not isNilOrEmpty("internalname")) then
table.insert(technical, container["internalname"])
end
if (not isNilOrEmpty("lootlist")) then
table.insert(technical, container["lootlist"])
end
table.insert(buffer, "<div style=\"text-align:right; color:grey; font-size:smaller; padding-top: 4px\">")
table.insert(buffer, table.concat(technical, "<br/>"))
table.insert(buffer, "</div>")
-- Finally, actually construct the tooltip
local tooltipArgs =
{
["clamp-viewport"] = "push"
}
if (display == "box") then
tooltipArgs["follow-cursor"] = "x"
tooltipArgs["update-event"] = "mousemove"
tooltipArgs["container-anchor"] = "bottom-left"
tooltipArgs["style"] = "width:100%; line-height:26px; vertical-align: middle; text-align:center; border-bottom:1px solid #7E5900; background:rgba(0,0,0,0.5);"
elseif (display == "row") then
tooltipArgs["anchor"] = "right"
tooltipArgs["style"] = "display:block"
end
tooltipArgs["container"] = wrapper;
tooltipArgs["content"] = table.concat(buffer)
-- Expand the template Tooltip_poe2 using the above arguments. This returns wikitext for us to use in the table
return frame:expandTemplate{ title = "Tooltip_poe2", args = tooltipArgs };
end
-- Build up a filter string for the container
function BuildContainerFilterString(container)
local buffer = { }
if (container["locked"] == "yes") then
table.insert(buffer, "locked")
end
if (not isNilOrEmpty(container["key_item"])) then
table.insert(buffer, "locked_key")
end
if (container["trapped"] == "yes") then
table.insert(buffer, "trapped")
end
if (container["hidden"] == "yes") then
table.insert(buffer, "hidden")
end
if (not isNilOrEmpty(container["steal_faction"])) then
table.insert(buffer, "stealing")
end
if (not isNilOrEmpty(container["inventory"])) then
table.insert(buffer, "fixed_loot")
end
if (not isNilOrEmpty(container["lootlist"])) then
table.insert(buffer, "random_loot")
end
return table.concat(buffer, ";")
end
-- Fetch the filter strings given input random loot, adding the filter strings to the table
-- (i.e. does not create a new table, does not return anything)
-- Loot should be a table of tables, containing { id, name, page, count } though only name is required.
-- Loot should ideally contain distinct values
function GetLootFilterStrings(loot)
for i = 1, #loot do
loot.filters = GetItemData(loot[i].id).filters or ""
end
end
-- Same as above, but determines the filter strings from a series of cargo queries
function GetLootFilterStringsCargo(loot)
local query =
{
-- Unfortunately we can't use CONCAT_WS since it's not an allowed SQL keyword on this wiki
{ name = "Item_poe1", fields = "name, LOWER(item_category) = a, LOWER(item_type) = b" },
{ name = "Shield_poe1", fields = "name, CONCAT('shield') = a, LOWER(shield_type) = b" },
{ name = "Armor_poe1", fields = "name, CONCAT('armor') = a, LOWER(armor_type) = b" },
{ name = "Weapon_poe1", fields = "name, CONCAT('weapon') = a, LOWER(weapon_type) = b" }
}
-- Create query string with loot items, prepend with "name IN ("," append with "")"
local searchStr = "name IN (\""..table.concat(common.tableSelectValues(loot, "page"), "\", \"").."\")"
local args =
{
where = searchStr,
groupBy = "name" -- <- Make distinct
}
local resultsCount = 0
-- Do cargo query in order of most-likely-to-return-results,
-- and check to see if we have all the results so we don't have to perform unnecessary queries
for i = 1, #query do
local results = cargo.query(query[i].name, query[i].fields, args)
resultsCount = resultsCount + #results
-- Add filters from results to the associated input loot item
for r = 1, #results do
local lootItem = common.tableGetTableWithKeyOfValue(loot, "page", results[r].name, true)
local str = ""
-- Add fields "a", "b"
if (not isNilOrEmpty(results[r].a)) then str = str..results[r].a..";" end
if (not isNilOrEmpty(results[r].b)) then str = str..results[r].b..";" end
-- Strip trailing semicolon
if (stringEndsWith(str, ";")) then
str = string.sub(str, 1, #str - 1)
end
lootItem.filters = str
end
if (resultsCount == #loot) then
break
end
end
end
-- OBSOLETE
-- Same as the above function, but changed to support fixed loot items
-- (making a generic function would be too much effort)
-- Changes from the above function:
-- The input loot parameter should be an array of item names
-- A query is performed based on the item name, not the internalname
-- Queries the "is_unique" field of items, as unique items are (for the most part) only found as fixed items
-- Returns an array of filter strings
function GetFixedLootFilterStrings(loot)
local query =
{
-- Unfortunately we can't use CONCAT_WS since it's not an allowed SQL keyword on this wiki
{ name = "Item_poe1", fields = "name, LOWER(item_category) = a, LOWER(item_type) = b, is_unique" },
{ name = "Shield_poe1", fields = "name, CONCAT('shield') = a, LOWER(shield_type) = b, is_unique" },
{ name = "Armor_poe1", fields = "name, CONCAT('armor') = a, LOWER(armor_type) = b, is_unique" },
{ name = "Weapon_poe1", fields = "name, CONCAT('weapon') = a, LOWER(weapon_type) = b, is_unique" }
}
-- Create query string with loot items, prepend with "name IN ("," append with "")"
local searchStr = "name IN (\""..table.concat(loot, "\", \"").."\")"
local args =
{
where = searchStr,
groupBy = "name" -- <- Make distinct
}
local resultsCount = 0
local filters = {}
-- Do cargo query in order of most-likely-to-return-results,
-- and check to see if we have all the results so we don't have to perform unnecessary queries
for i = 1, #query do
local results = cargo.query(query[i].name, query[i].fields, args)
-- Add filters from the results to the output at the associated index
for r = 1, #results do
-- Loop over input loot items to find the item associated with this result
-- (the results are almost always in a different order than the input loot)
for k = 1, #loot do
if (results[r].name == loot[k]) then
filters[k] = ""
-- Add fields "a", "b"
if (not isNilOrEmpty(results[r].a)) then filters[k] = filters[k]..results[r].a..";" end
if (not isNilOrEmpty(results[r].b)) then filters[k] = filters[k]..results[r].b..";" end
-- Add "unique_item" if the item is unique
if (results[r].is_unique == "yes") then
filters[k] = filters[k].."unique_item"
end
-- Strip trailing semicolon
if (stringEndsWith(filters[k], ";")) then
filters[k] = string.sub(filters[k], 1, #filters[k] - 1)
end
resultsCount = resultsCount + 1
end
end
end
-- If the amount of processed results matches the input loot, return
if (resultsCount == #loot) then
break
end
end
return filters
end
-- Lootlist functions
-- Returns a collection of items (an array of item ID strings) by evaluating the loot list
-- Must be passed the lootlist to evaluate (directly from the cargo query). The rolls will be made based on a set of initialization parameters for Xorshift (both set in the above #invoke'd functions)
-- The order of the returned items is determined by the evaluation order. The in-game order is sorted based on the item type as well as its value, but to avoid slowing down evaluation we won't bother doing this.
-- The recursionDepth parameter is for logging purposes only
function EvaluateLootList(lootlist, recursionDepth)
local items = { } -- The resulting items, an array with a table of { item, page } for each item
local done = false; -- True when we've added an item from this list, signalling that we should add no more
local roll = nil
if (lootlist == nil or lootlist.items == nil) then
--mw.log("Cannot evaluate an empty lootlist, returned nil")
return items
end
if (recursionDepth == nil) then recursionDepth = 0 end
local prefix = string.rep("\t", recursionDepth)
-- This isn't required if we have predefined rolls, but IS if we instead use the RNG state
local totalWeight = lootlist["total_weight"]
--mw.log(prefix.."Total weight is "..totalWeight)
-- Make roll from 0 to totalWeight (exclusive) using Xorshift state.
-- If the state values are correct, this will mimic the exact rolls as made in game
roll = Xorshift128_NextUIntMax(totalWeight)
local cumulativeWeight = 0; -- Used for weighted random selection
--mw.log(prefix.."### "..lootlist[1].lootlist.." - Rolled " .. roll.." of d"..totalWeight.." ###")
-- Loop over the LootItems in the lootlist
for _, lootItem in ipairs(lootlist.items) do
--mw.log(prefix.."["..lootItem.position.."] "..common.default(lootItem.item, "<empty>").." (Weight: "..lootItem.weight..", Always: "..lootItem.always..", Count: "..tostring(lootItem.count)..")")
-- Whether we should add this to the output items
local shouldAdd = false
-- Always add this item
if (lootItem.always == true) then
shouldAdd = true
--mw.log(prefix.."\tAdding this item (always)")
else
-- Add this LootItem's weight to the cumulative weight
cumulativeWeight = cumulativeWeight + lootItem.weight
--mw.log(prefix.."\tCumulative weight is now "..cumulativeWeight)
-- If we've reached the value defined in the roll, pick this one!
if (roll < cumulativeWeight and done ~= true) then
--mw.log(prefix.."\tAdding this item ("..roll.." is now < "..cumulativeWeight..")")
shouldAdd = true
done = true
end
end
-- If the item is actually present (and isn't an empty value), and if we should add it
if (lootItem.id ~= nil and shouldAdd) then
--mw.log(prefix.."\tEvaluating "..common.default(lootItem.item, "<empty>").." "..lootItem.count.." times")
-- Repeat adding the item <count> times
for count = 1, tonumber(lootItem.count) do
-- Recurse if the LootItem is a lootlist
if (lootItem.is_lootlist == true) then
--mw.log(prefix.."\tLootItem is LootList! Begin recursion...")
-- Evaluate child LootList with the next roll
local childLootList = GetLootlist(lootItem.id)
childItems = EvaluateLootList(childLootList, recursionDepth + 1)
-- Add all resulting (child)items to the results
if (childItems ~= nil) then
for i=1, #childItems do
items[#items + 1] = childItems[i]
end
end
--mw.log(prefix.."\tEnd recursion!")
-- or just add the item if it's an item
else
--mw.log(prefix.."\tAdded 1x "..common.default(lootItem.item, "<empty>"))
items[#items + 1] = lootItem.id
end
end -- end for
end -- end if
end -- end for
return items
end
-- "Stack" duplicate items, adding a count field and removing empty items.
-- Note that these won't be valid sized stacks (we don't store MaxStackSizes on the wiki)
-- ...we don't really need or want them to be since it's very unlikely that a container has multiple full stacks of something
-- Assumes the input is an array of item ID strings [ "id1", "id2", "id3" ]
-- Outputs a new array of items in the format { id, count }
function StackItems(items)
local output = { }
-- Loop through all the items in the passed table
for _, item in ipairs(items) do
-- Get the existing table with the same item ID from the output (if it exists)
local outputItem = common.tableGetTableWithKeyOfValue(output, "id", item)
-- Add 1 to the count if the output already contains an item with this ID
if (outputItem ~= nil) then
outputItem.count = outputItem.count + 1
-- Otherwise create a new item object with { id, count } to add to the output
elseif (item ~= nil and item ~= "") then
outputItem = { ["id"] = item, ["count"] = 1 }
table.insert(output, outputItem)
end
end
return output
end
-- Sort items *in-place* in the same fashion as in game
-- Items can either be an array of ids or an array of item objects (containing the id)
function SortItems(items)
table.sort(items, SortItem)
end
local EQUIPPABLES = { "armor", "accessory", "clothing", "weapon", "grimoire", "pet", "shield" }
-- Receives two arguments, returning true if the first argument should come first in the sorted array
-- As reference, in C# a sort function would return a negative integer if the first argument comes first, and a positive integer if the second argument comes first
function SortItem(a, b)
-- a nil, b comes first
if (a == nil)
then return false
end
-- b nil, a comes first
if (b == nil)
then return true
end
-- Fetch item data required to sort
local ad = GetItemData(a.id or a)
local bd = GetItemData(b.id or b)
-- Filter type, low-to-high (NONE = 0, WEAPONS = 1, ARMOR = 2, DEPRECATED_AMMO = 4, CLOTHING = 8, CONSUMABLES = 16, INGREDIENTS = 32, QUEST = 64, MISC = 128)
-- If the difference between the filterTypes is:
-- Negative - a comes first, return true
-- Positive - b comes first, return false
-- 0 - Move to next rule
local flag1 = ad.filterType - bd.filterType
if (flag1 ~= 0) then
return flag1 < 0
end
-- Consumable type, low-to-high, only if both items are consumable (Ingestible = 0, Scroll = 1, Figurine = 2, Trap = 3, Potion = 4, Drug = 5, Count = 6, None = 7)
-- If the difference between the consumable types is:
-- Negative - a comes first, return true
-- Positive - b comes first, return false
-- 0 - Move to next rule
if (ad.consumableType ~= nil and bd.consumableType ~= nil) then
local flag2 = ad.consumableType - bd.consumableType
if (flag2 ~= 0) then
return flag2 < 0
end
end
-- If equippable, secondary weapon slot only items (i.e. shields) are sorted after other equippables
if (IsItemEquippable(ad) and IsItemEquippable(bd)) then
-- The in-game equivalent is a.SecondaryWeaponSlot && !a.PrimaryWeaponSlot,
-- though we don't need to store this since we know only shields are secondary slot only
local isAShield = string.find(ad.filters, "shield", 1, true)
local isBShield = string.find(bd.filters, "shield", 1, true)
-- If a is a shield and b isn't a shield, b comes first (return false)
if (isAShield and not isBShield) then
return false
end
-- If b is a shield and a isn't a shield, a comes first (return true)
if (isBShield and not isAShield) then
return true
end
-- If both are shields or both aren't shields, continue below
end
-- Value, high-to-low
-- If the difference between b and a is:
-- Negative (a is higher) - a comes first, return true
-- Positive (b is higher) - b comes first, return false
-- 0 - Continue below
local flag3 = bd.value - ad.value
if (flag3 ~= 0) then
return flag3 < 0
end
return ad.name < bd.name
end
function IsItemEquippable(itemData)
for i = 1, #EQUIPPABLES do
if (string.find(itemData.filters, EQUIPPABLES[i], 1, true)) then
return true
end
end
return false
end
-- Adds name, page, and filters to items from the data module
-- Assumes items is an array of [ { id, count }, ... ]
function AddItemData(items)
if (items == nil) then return end
for i = 1, #items do
local data = GetItemData(items[i].id)
if (data == nil) then
items[i].name = items[i].id
else
items[i].name = data.name;
items[i].page = data.page;
items[i].filters = data.filters
end
end
end
-- Returns a table that contains all items in a specific lootlist
function GetLootlist(name)
-- Return nil (don't throw error) if name isn't passed
if (isNilOrEmpty(name)) then
return nil
end
-- Ensure the data module is loaded
if (checkDataLoaded() == false) then
data = mw.loadData("Module:Lootlist/data")
end
local lootlist = data.lootlists[name]
-- If a lootlist with this name wasn't found, throw error
if (lootlist == nil) then
error("A lootlist with the name "..name.." was not found, or returned no results"); return
end
return lootlist
end
-- Same as above, but queries the Lootitem_poe1 cargo table instead
function GetLootlistCargo(name)
-- Return nil (don't throw error) if name isn't passed
if (isNilOrEmpty(name)) then
return nil
end
local tables = "Lootitem_poe1"
local fields = "lootlist, position, item, page, is_lootlist, count, weight, always"
local args =
{
where = "lootlist = '" .. name .. "'",
orderBy = "position",
limit = 5000,
}
-- Do cargo query
local query = cargo.query(tables, fields, args)
-- If no items are found, throw error
if (next(query) == nil) then
error("A lootlist with the name "..name.." was not found, or returned no results"); return
end
return query
end
-- Get data for an item with a specific ID from the data module
-- The data is a table with the fields { id, name, page, filters, etc }
-- Keep in mind that the data returned will be READ ONlY.
function GetItemData(id)
-- Ensure the data module is loaded
if (checkDataLoaded() == false) then
data = mw.loadData("Module:Lootlist/data")
end
local item = data.items[id:lower()]
-- If an item with this ID wasn't found, throw error
if (item == nil) then
return nil
--error("An item with the ID "..id.." was not found"); return
end
return item
end
function GetItemDataByName(name)
-- Ensure the data module is loaded
if (checkDataLoaded() == false) then
data = mw.loadData("Module:Lootlist/data")
end
local item = nil
local id = nil
for k,v in pairs(data.items) do
if (v.name == name) then
item = v
id = k
break
end
end
-- If an item with this name wasn't found, throw error
if (item == nil) then
error("An item with the name "..name.." was not found"); return
end
-- Remake to enable read-write
local itemRw =
{
["id"] = id,
["name"] = item.name,
["page"] = item.page or nil,
["filters"] = item.filters
}
return itemRw
end
--- Calculates the total weight of the lootlist, a direct output from a Lootitem_poe1 cargo query
function GetTotalWeight(lootlist)
local totalWeight = 0
for _, lootItem in next, lootlist do
if (lootItem.always ~= "1") then
totalWeight = totalWeight + lootItem.weight
end
end
return totalWeight
end
-- Check to see if the data module is loaded
function checkDataLoaded()
if (data == nil) then
return false
end
return true
end
-- Game helper functions
-- Generates the seed in the same way the game does. Expects the xz position of the container and the day of the month (1-20)
-- (int)((double)loot.transform.position.x + (double)loot.transform.position.z) * GameState.s_playerCharacter.name.GetHashCode() + day)
function CalculateSeedForContainer(x, z, day)
if (tonumber(x) == nil or tonumber(z) == nil or tonumber(day) == nil) then
error("One or more of the parameters x, z, day for CalculateSeedForContainer is nil or NaN")
return nil
end
-- Truncate to strip decimal portion
local t, _ = math.modf(x + z)
-- Calculate the seed
local long = t * 1413436758 + day
-- Replicate C# "long" to "int" casting, converting the signed 64-bit value (default in Lua) to a signed 32-bit integer
-- This effectively removes the upper 32 bits, but preserves the sign
return (long + 2^31) % 2^32 - 2^31
-- A different way to do the same using the bit32 library
--[[
if (a < 0) then
b = -bit32.band(0xFFFFFFFF, -a)
else
b = bit32.band(0xFFFFFFFF, a)
end
]]--
end
-- World position to map position
-- The world background quad is placed on the XZ-plane with the camera above facing down (-Y) onto it, however the camera is actually rotated by 180 + 45 degrees on the Y axis (and 40 degrees on the X axis) such that:
-- The world +X axis is towards the bottom left of the screen (as opposed to right of the screen)
-- The world -X axis is towards the top right of the screen (as opposed to left of the screen)
-- The world +Z axis is towards the bottom right of the screen (as opposed to the top of the screen)
-- The world -Z axis is towards the top left of the screen (as opposed to the bottom of the screen)
-- This helper function will convert a world xz position to a map position by rotating it, then applying a scale to a target size.
-- Vector2 coordinates are referred to as XZ instead of XY since the map doesn't make use of the Y axis
-- Parameters:
-- worldX, worldZ: The input world position to be converted
-- worldBotLeftX, worldBotLeftZ, worldTopRightX, worldTopRightZ: The bottom left and top right positions of the scene background quad. This is LevelInfo.m_backgroundQuadVertex00 and LevelInfo.m_backgroundQuadVertex11 respectively
-- mapWidth, mapHeight: The target size of the map. The aspect ratio should match the input coordinates, or will be stretched dispropotionately. The target size should typically be 1084 x 610 to match the in-game map size, but can be any increment larger or smaller.
function WorldToMapPosition(worldX, worldZ,
worldBotLeftX, worldBotLeftZ,
worldTopRightX, worldTopRightZ,
mapWidth, mapHeight)
-- Rotate the world background corners -45 degrees (Y) to convert it to the camera axis
local blx, bly, blz = quaternionMultVector(0, -0.3826835, 0, 0.9238795, worldBotLeftX, 0, worldBotLeftZ)
local trx, try, trz = quaternionMultVector(0, -0.3826835, 0, 0.9238795, worldTopRightX, 0, worldTopRightZ)
-- Do the same with the input world position
local wx, wy, wz = quaternionMultVector(0, -0.3826835, 0, 0.9238795, worldX, 0, worldZ)
-- Scale the rotated position from world units to map units
local mx = scale(wx, blx, trx, 0, mapWidth)
local my = scale(wz, trz, blz, 0, mapHeight)
return mx, my
end
-- Random number generation functions
-- Integer RNG is 1-to-1 with Unity, while floating point has a slight variation on the precision
-- Xorshift128 state (globals)
-- x is seed (only if positive), yzw are generated based on the seed.
x = 0; y = 0; z = 0; w = 0
MT19937 = 1812433253
INT32_MIN = -2147483648 -- 0x80000000
INT32_MAX = 2147483647 -- 0x7FFFFFFF
UINT32_MAX = 4294967295 -- 0xFFFFFFFF
function p.Xorshift128_Tests()
local data = mw.loadData("Module:Lootlist/data")
Xorshift128_InitSeed(1234)
-- Print some integers
for i = 1, 5 do
mw.log(Xorshift128_Next())
end
-- Print some ranged integers
-- 0 to max
p.Xorshift128_IntegerTests(0, INT32_MAX)
-- 0 to min
p.Xorshift128_IntegerTests(0, INT32_MIN)
-- Min to max
p.Xorshift128_IntegerTests(INT32_MIN, INT32_MAX)
-- Max to min
p.Xorshift128_IntegerTests(INT32_MAX, INT32_MIN)
-- Min to min (error)
p.Xorshift128_IntegerTests(INT32_MIN, INT32_MIN)
-- Print some floating pointss
for i = 1, 5 do
mw.log(Xorshift128_NextFloat())
end
-- Print some ranged floating points
for i = 1, 5 do
mw.log(Xorshift128_NextFloat())
end
--[[
local limit = 0
local equal = 0
local notEqual = 0
-- Compare generated states with pregenerated states
for k, v in pairs(data.states) do
limit = limit + 1
Xorshift128_InitSeed(k)
-- Confirm that state is equal to pregen
if (x ~= v[1] or y ~= v[2] or z ~= v[3] or w ~= v[4]) then
notEqual = notEqual + 1
else
equal = equal + 1
end
mw.log("Seed: "..k)
mw.log("Pregen:".."x="..v[1]..", y="..v[2]..", z="..v[3]..", w="..v[4])
mw.log("Newgen:".."x="..x..", y="..y..", z="..z..", w="..w)
if (limit == 50) then
break
end
end
mw.log("Pregenerated vs generated states\n\tEqual: "..equal.."\n\tNot equal: "..notEqual)
]]--
end
function p.Xorshift128_IntegerTests(min, max)
mw.log("--- "..min.." to "..max.." ---")
for i = 1, 5 do
mw.log(Xorshift128_NextIntRange(min, max))
end
end
function p.Xorshift128_FloatingPointTests(min, max)
mw.log("--- "..min.." to "..max.." ---")
for i = 1, 5 do
mw.log(Xorshift128_NextFloatRange(min, max))
end
end
-- Initialize Xorshift using a signed integer seed, calculating the state values using the initialization method from Mersenne Twister (MT19937)
-- https://en.wikipedia.org/wiki/Mersenne_Twister#Initialization
function Xorshift128_InitSeed(seed)
if (seed == nil or type(seed) ~= "number") then
error("Seed parameter is not a number");
return
end
-- Convert the input signed int to an unsigned int
x = overflow32U(seed)
-- Break the 1812433253 into two prime factors (1289 * 1406077 = 1812433253) and perform two multiplication operations with a bitwise right shift on each factor to convert the result into a unsigned 32-bit int
-- This is the same as the following Lua operation (but much faster, given we know the prime factors in advance)
-- y = uintMultiply(MT19937, x) + 1
-- and primarily C#'s:
-- y = (uint)(MT19937 * x + 1)
y = bit32.rshift(1289 * bit32.rshift(1406077 * x, 0) + 1, 0)
z = bit32.rshift(1289 * bit32.rshift(1406077 * y, 0) + 1, 0)
w = bit32.rshift(1289 * bit32.rshift(1406077 * z, 0) + 1, 0)
end
-- Initialize Xorshift using a seed (supported via the lookup table Module:Lootlist/data)
-- NO LONGER USED - We can generate 1-to-1 integers now! :)
function Xorshift128_InitSeedPregen(seed)
if (type(seed) ~= "number") then
error("Seed parameter is not a number"); return
elseif (data == nil or data.states == nil) then
error("Pregenerated state data not loaded!"); return
end
local state = data.states[seed]
if (state == nil or state[1] == nil or state[2] == nil or state[3] == nil or state[4] == nil) then
error("Pregenerated states for seed "..seed.." in Modules:Lootlist/data not found or invalid"); return
end
Xorshift128_Init(state[1], state[2], state[3], state[4])
end
-- Initialize the entire Xorshift state explicitly
function Xorshift128_Init(x, y, z, w)
_G.x = x; _G.y = y; _G.z = z; _G.w = w;
end
-- Generate a random unsigned 32-bit integer value (0 to 4,294,967,295)
-- This is the equivalent of a uint in C#.
function Xorshift128_Next()
-- C equivalent: x ^ (x << 11)
local t = bit32.bxor(x, bit32.lshift(x, 11))
x = y; y = z; z = w
-- C equivalent: w = w ^ (w >> 19) ^ t ^ (t >> 8)
w = bit32.bxor(w, bit32.rshift(w, 19), t, bit32.rshift(t, 8))
return w
end
-- Alias of the above
function Xorshift128_NextUInt()
return XORShift128_Next()
end
-- Generate random unsigned 32-bit integer value in the range 0 (inclusive) to max (exclusive)
function Xorshift128_NextUIntMax(max)
if (max == 0) then
return 0
else
return Xorshift128_Next() % max
end
end
-- Generate random unsigned 32-bit integer value in the range min (inclusive) to max (exclusive)
function Xorshift128_NextUIntRange(min, max)
if (max - min == 0) then
return min
else
return min + Xorshift128_Next() % (max - min)
end
end
-- Mod the result by int.MaxValue (2,147,483,647) to wrap the value to be in the positive 32-bit signed integer range.
function Xorshift128_NextInt()
return Xorshift128_Next() % 2147483647
end
function Xorshift128_NextIntRange(min, max)
if (max - min == 0) then
return min
end
local r = Xorshift128_Next()
if (max < min) then
return min - math.fmod(r, (max - min))
else
return min + math.fmod(r, (max - min))
end
end
function Xorshift128_NextFloat()
return 1.0 - Xorshift128_NextFloatRange(0.0, 1.0)
end
function Xorshift128_NextFloatRange(min, max)
return (min - max) * (bit32.lshift(Xorshift128_Next(), 9) / 0xFFFFFFFF) + max;
end
-- Binary logic operations
-- Signed 64-bit to signed 32-bit
-- Replicate C# unchecked "long" to "int" casting, converting the signed 64-bit value (default in Lua) to a signed 32-bit integer by means of overflow
-- This effectively removes the upper 32 bits, but preserves the sign
function overflow32(long)
-- Using integer arithmetic
--return (long + 2^31) % 2^32 - 2^31
-- Using the bit32 library
if (bit32.band(long, 0x80000000) ~= 0) then
return (bit32.band(bit32.bnot(long), 0xFFFFFFFF) + 1) * -1
else
return bit32.band(0xFFFFFFFF, long)
end
end
-- Same as above, but does not preserve sign
function overflow32U(long)
return bit32.band(0xFFFFFFFF, long)
end
-- This performs multiplication on Lua's double-precision floating point values
-- but generates a result as if the inputs were instead unsigned 32-bit values
-- This is required to emulate uint32 overflow correctly. otherwise, higher
-- order bits are simply truncated and discarded
function uintMultiply(a, b)
a = a % 4294967296 -- Wrap to length of uint
b = b % 4294967296 -- for both sides
local ah, al = math.floor(a / 65536), a % 65536
local bh, bl = math.floor(b / 65536), b % 65536
local high = ((ah * bl) + (al * bh)) % 65536
return ((high * 65536) + (al * bl)) % 4294967296
end
-- Math operations
-- Rotates a vector by a rotation, returning the rotated vector
function quaternionMultVector(qx, qy, qz, qw, vx, vy, vz)
local x = qx * 2;
local y = qy * 2;
local z = qz * 2;
local xx = qx * x;
local yy = qy * y;
local zz = qz * z;
local xy = qx * y;
local xz = qx * z;
local yz = qy * z;
local wx = qw * x;
local wy = qw * y;
local wz = qw * z;
-- Return the rotated vector
rx = (1 - (yy + zz)) * vx + (xy - wz) * vy + (xz + wy) * vz;
ry = (xy + wz) * vx + (1 - (xx + zz)) * vy + (yz - wx) * vz;
rz = (xz - wy) * vx + (yz + wx) * vy + (1 - (xx + yy)) * vz;
return rx, ry, rz;
end
-- Invoke entry point, typically you should instead use this with:
-- {{subst:#invoke:Lootlist|scale|...}}
function p.scale(number, fromMin, fromMax, toMin, toMax)
return scale(number, fromMin, fromMax, toMin, toMax)
end
-- Scales a float from one range to another
-- For example, scaling a float value 0.5 in the range 0-1 to 0-100 will return 50
function scale(number, fromMin, fromMax, toMin, toMax)
return (toMax - toMin) * (number - fromMin) / (fromMax - fromMin) + toMin
end
-- Logical and misc operations
function log(str)
if (LOGGING_ENABLED == true) then mw.log(str) end
end
-- Ordered table iterator, allow to iterate on the natural order of the keys of a
-- table.
function __genOrderedIndex( t )
local orderedIndex = {}
for key in pairs(t) do
table.insert( orderedIndex, key )
end
table.sort( orderedIndex )
return orderedIndex
end
function orderedNext(t, state)
-- Equivalent of the next function, but returns the keys in the alphabetic
-- order. We use a temporary ordered key table that is stored in the
-- table being iterated.
local key = nil
--print("orderedNext: state = "..tostring(state) )
if state == nil then
-- the first time, generate the index
t.__orderedIndex = __genOrderedIndex( t )
key = t.__orderedIndex[1]
else
-- fetch the next value
for i = 1,table.getn(t.__orderedIndex) do
if t.__orderedIndex[i] == state then
key = t.__orderedIndex[i+1]
end
end
end
if key then
return key, t[key]
end
-- no more value to return, cleanup
t.__orderedIndex = nil
return
end
function orderedPairs(t)
-- Equivalent of the pairs() function on tables. Allows to iterate
-- in order
return orderedNext, t, nil
end
return p