Module:Lootlist

-- Module:Lootlist -- Author: Macklin -- Description: Used to generate/evaluate random lootlists and provides formatting support for Template:Container

local p = {} local cargo = mw.ext.cargo local bit32 = require("bit32") --local data = mw.loadData("Module:Lootlist/data")

local pageTitle = mw.title.getCurrentTitle;

-- Container functions

-- Entry point from Template:Container. This is a complex multi-function invoke, taking about 100ms per call -- 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 function p.Container(frame) -- Fetch the parameters that were originally passed to the template containing the #invoke to this function local args = { } -- We can't modify the frame args since they're read only, but we can make a copy of it	for k, v in pairs(frame:getParent.args) do		args[k] = v	end -- Do checks to see if some required parameters are missing if		TableValueIsNilOrEmpty(args, "name")		then return elseif	TableValueIsNilOrEmpty(args, "position_x")	then return elseif	TableValueIsNilOrEmpty(args, "position_z")	then return end -- Fetch the root lootlist with this name from cargo local lootlist = GetLootlist(args["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 next(lootlist) ~= 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(args["position_x"]) local posz = tonumber(args["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 and stack the resulting items local loot = EvaluateLootList(lootlist) loot = StackItems(loot) -- Add filter strings GetLootFilterStrings(loot) args["day_"..day.."_loot"] = loot; end end -- Add filter strings for fixed loot local result = BuildContainerTable(frame, args) ContainerCargoStore(frame, args) return result 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, args)

-- The day_x_loot fields need to be converted from tables into two separate "List of String"'s before actually doing the cargo_store -- So here we make a copy of the args without doing mw.clone (since this will unnecessarily duplicate the loot tables) local storeArgs = { } for k, v in pairs(args) do		local day = string.match(k, "day_(%d+)_loot.*") -- For all day_x_loot tables if (day ~= nil) 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				if (i > 1) then lootStr = lootStr..";"..item.page lootQtyStr = lootQtyStr..";"..item.count else lootStr = lootStr..item.page lootQtyStr = lootQtyStr..item.count end end storeArgs["day_"..day.."_loot"] = lootStr storeArgs["day_"..day.."_loot_qty"] = lootQtyStr -- For all other values else storeArgs[k] = v		end end

-- Do cargo_store -- Only store if called from main namespace to prevent documentation examples being added to the cargo table if (pageTitle ~= nil and pageTitle.namespace == 0) then -- Todo: Call cargo store parser function end end

-- This function constructs the wikitext to be returned in Container and is what is displayed on the page. function BuildContainerTable(frame, args) local tooltip = BuildContainerTooltip(frame, args) local filterStr = BuildContainerFilterString(args) local header = ""..args["name"].."" if (not isNilOrEmpty(args["description"])) then header = header.." "..args["description"] end local mapNodeString = "" if (not isNilOrEmpty(args["mapnode"])) then mapNodeString = args["mapnode"].." ↲" end -- Begin creating loot container local tbl = mw.html.create("div") :addClass("loot-container") :attr("id", args["internalname"]) :attr("data-name", args["name"]) :attr("data-group", args["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(args["inventory"]) and not isNilOrEmpty(args["inventory_qty"])) then -- Split the inventory and inventory_qty fields local fixedInventoryItems = mw.text.split(args["inventory"], ";", true) local fixedInventoryQty = mw.text.split(args["inventory_qty"], ";", true) local fixedInventoryFilters = GetFixedLootFilterStrings(fixedInventoryItems) 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", fixedInventoryFilters[i]) -- Only include quantity if it's > 1 if (tonumber(fixedInventoryQty[i]) > 1) then li:wikitext(fixedInventoryQty[i], "x ") end -- Add link li:wikitext("", fixedInventoryItems[i], "") -- Add list item to list fixedItems:node(li) end end -- Add random items row if (not isNilOrEmpty(args["lootlist"])) then local containerID = "" if (args["guid"] ~= nil) then containerID = string.sub(args["guid"], 1, 8) elseif (args["internalname"] ~= nil) then containerID = args["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:?") :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 = args["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(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 (isNotNilOrEmpty(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 li:wikitext("", item.page, "") -- Add list item to list dayLoot:node(li) end end end return tostring(tbl) end

-- Build up a filter string for the container function BuildContainerFilterString(args) local buffer = { } if (args["locked"] == "yes") then table.insert(buffer, "locked") end if (isNotNilOrEmpty(args["key_item"])) then table.insert(buffer, "locked_key") end if (args["trapped"] == "yes") then table.insert(buffer, "trapped") end if (args["hidden"] == "yes") then table.insert(buffer, "hidden") end if (isNotNilOrEmpty(args["steal_faction"])) then table.insert(buffer, "stealing") end if (isNotNilOrEmpty(args["inventory"])) then table.insert(buffer, "fixed_loot") end if (isNotNilOrEmpty(args["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 { item, page, count } though only item is required. -- Loot should ideally contain distinct values function GetLootFilterStrings(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(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 = TableGetTableWithKeyOfValue(loot, "page", results[r].name, true) local str = "" -- Add fields "a", "b" if (isNotNilOrEmpty(results[r].a)) then str = str..results[r].a..";" end if (isNotNilOrEmpty(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

-- 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 output = {} -- 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 output[k] = "" -- Add fields "a", "b" if (isNotNilOrEmpty(results[r].a)) then output[k] = output[k]..results[r].a..";" end if (isNotNilOrEmpty(results[r].b)) then output[k] = output[k]..results[r].b..";" end -- Add "unique_item" if the item is unique if (results[r].is_unique == "yes") then output[k] = output[k].."unique_item" end -- Strip trailing semicolon if (stringEndsWith(output[k], ";")) then output[k] = string.sub(output[k], 1, #output[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 output end

-- Build up the icon row of the container function BuildContainerIconRow(args) local buffer = { "" } -- Container contains fixed loot if (not isNilOrEmpty(args["inventory"])) then table.insert(buffer, "&#x1F4CC; ") end if (not isNilOrEmpty(args["lootlist"])) then table.insert(buffer, "&#x1F3B2; (")		table.insert(buffer, args["lootlist"])		table.insert(buffer, ") ") end -- Locked if (args["locked"] == "yes") then table.insert(buffer, " (")		local keyLink = nil;		-- Key present		if (not isNilOrEmpty(args["key_item"])) then			keyLink = "Key"		end		-- Key required		if (args["key_required"] == "yes") then			table.insert(buffer, keyLink)		-- Key not required, or not present		else			table.insert(buffer, args["lock_difficulty"])			if (keyLink ~= nil) then				table.insert(buffer, ", ")				table.insert(buffer, keyLink)			end		end		table.insert(buffer, ") ") end -- Trapped if (args["trapped"] == "yes" and not isNilOrEmpty(args["trap_difficulty"])) then table.insert(buffer, " (")		table.insert(buffer, args["trap_difficulty"])		table.insert(buffer, ") ") end -- Hidden if (args["hidden"] == "yes" and not isNilOrEmpty(args["detect_difficulty"])) then table.insert(buffer, " (")		table.insert(buffer, args["detect_difficulty"])		table.insert(buffer, ") ") end -- Stealing if (args["steal_faction"] ~= nil and string.lower(args["steal_rep"]) ~= "none") then table.insert(buffer, " (")		table.insert(buffer, args["steal_faction"])		table.insert(buffer, ")") end return table.concat(buffer) 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, args) local buffer = { "" } -- Container contains fixed loot if (not isNilOrEmpty(args["inventory"])) then table.insert(buffer, "&#x1F4CC; Contains fixed loot always present on opening\n") end if (not isNilOrEmpty(args["lootlist"])) then table.insert(buffer, "&#x1F3B2; Contains random loot — One of the following 20 sets of items is present depending on the day of the month\n") end table.insert(buffer, "\r\n") -- Container is locked if (args["locked"] == "yes") then local lockDifficulty = tonumber(args["lock_difficulty"]) local lockKey = args["key_item"] local isKeyRequired = args["key_required"] == "yes" local startTime = os.clock table.insert(buffer, " Container is locked") -- 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 (isNotNilOrEmpty(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 (args["trapped"] == "yes" and not isNilOrEmpty(args["trap_difficulty"])) then local trapItem = args["trap_item"] table.insert(buffer, " Container is trapped (Difficulty ")		table.insert(buffer, args["trap_difficulty"])		table.insert(buffer, ") \n* Requires at least Mechanics ") table.insert(buffer, args["trap_difficulty"]) table.insert(buffer, " to disable") if (args["trap_item"] ~= nil) then table.insert(buffer, "\n* Disabling gives a ")			table.insert(buffer, args["trap_item"])			table.insert(buffer, "") end table.insert(buffer, "\n") end -- Container is hidden if (args["hidden"] == "yes" and args["detect_difficulty"] ~= nil) then local detectDifficulty = tonumber(args["detect_difficulty"]) -- Trap is hidden table.insert(buffer, " ") table.insert(buffer, ternary(args["trapped"] == "yes" and args["trap_difficulty"] ~= nil, "Trap", "Container")) table.insert(buffer, " is hidden (Difficulty ")		table.insert(buffer, detectDifficulty)		table.insert(buffer, ") \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(args["steal_faction"])) then table.insert(buffer, " Container is owned by ")		table.insert(buffer, args["steal_faction"])		table.insert(buffer, "\n* Being caught stealing incurs a ") local rep = string.lower(args["steal_rep"]) if		(rep == "veryminor")	then rep = "Very Minor (1)" elseif	(rep == "minor")		then rep = "Minor (2)" elseif	(rep == "average")		then rep = "Average (4)" elseif	(rep == "major")		then rep = "Major (6)" elseif	(rep == "verymajor")	then rep = "Very Major (8)" end table.insert(buffer, rep) table.insert(buffer, " reputation loss with this faction\n") if (args["attack_thief"] == "yes") then table.insert(buffer, "\n* Nearby faction members become hostile") end if (args["ally_attack_thief"] == "yes") then table.insert(buffer, "\n* Nearby allied faction members become hostile") end end -- Add technical information local technical = { } if (isNotNilOrEmpty("internalname")) then table.insert(technical, args["internalname"]) end if (isNotNilOrEmpty("lootlist")) then table.insert(technical, args["lootlist"]) end table.insert(buffer, "") table.insert(buffer, table.concat(technical, " ")) table.insert(buffer, " ") -- Finally, actually construct the tooltip local tooltipArgs = {		["follow-cursor"] = "x", ["update-event"] = "mousemove", ["container-anchor"] = "bottom-left", ["clamp-viewport"] = "push", ["style"] = "width:100%; line-height:26px; vertical-align: middle; text-align:center; border-bottom:1px solid #7E5900; background:rgba(0,0,0,0.5);" }	tooltipArgs["container"] = BuildContainerIconRow(args) 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

-- Lootlist functions

-- Returns a collection of items (a table consisting of the fields item, page, count) 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 next(lootlist) == 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 = GetTotalWeight(lootlist) --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) do --mw.log(prefix.."["..lootItem.position.."] "..default(lootItem.item, " ").." (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 == "1") 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.item ~= nil and shouldAdd) then --mw.log(prefix.."\tEvaluating "..default(lootItem.item, " ").." "..lootItem.count.." times") -- Repeat adding the item times for count = 1, tonumber(lootItem.count) do   			-- Recurse if the LootItem is a lootlist if (lootItem.is_lootlist == "1") then --mw.log(prefix.."\tLootItem is LootList! Begin recursion...") -- Evaluate child LootList with the next roll local childLootList = GetLootlist(lootItem.item) 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 "..default(lootItem.item, " ")) items[#items + 1] = { item = lootItem.item, page = lootItem.page } 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 input items are in the following format { item, page } -- Outputs a new table containing the stacked items function StackItems(items) local output = { } -- Loop through all the items in the passed table for _, lootItem in ipairs(items) do		-- Get the existing table with the same item name from the output (if it exists) local outputItem = TableGetTableWithKeyOfValue(output, "item", lootItem.item) -- Add 1 to the count if the output already contains an item with this name if (outputItem ~= nil) then outputItem.count = outputItem.count + 1 -- Otherwise create a new clone of this item for the output elseif (lootItem ~= nil and lootItem.item ~= nil and lootItem.item ~= "") then outputItem = mw.clone(lootItem) outputItem.count = 1; table.insert(output, outputItem) end end return output 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 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

--- 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

-- 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

-- Some common table operations

-- Loops through each of the tables within the array/table, selecting the value of the key and adding it to a table to be returned -- If the child table does not contain a value with the key, or if the child table is null, a nil value is substituted (i.e. no checking is done) function TableSelectValues(rootTable, key) local result = { } if (rootTable ~= nil) then for _, childTable in ipairs(rootTable) do			if (childTable ~= nil) then table.insert(result, childTable[key]) else table.insert(result, nil) end end end return result end -- Returns true if the table/array contains a table that has a key with a specific value, otherwise returns false function TableHasTableWithKeyOfValue(table, key, value) local result = TableGetTableWithKeyOfValue(table, key, value) return (result ~= nil) end -- Returns a table from the table/array that contains a key with a specific value, otherwise returns nil -- For example calling TableGetTableWithKeyOfValue(test, "id", 1) for the following table: test = { first = { id = 1, data = "somedata" }, second = { id = 2, data = "somemoredata" }, } Will return: { id = 1, data="somedata" } -- function TableGetTableWithKeyOfValue(tbl, key, value, caseInsensitive) -- Ensure that the key or table isn't nil if (key == nil or tbl == nil) then return nil end -- Default caseInsensitive to false if (caseSensitive == nil) then caseSensitive = false end -- Loop over tables in table for i = 1, #tbl do		-- Check to see if the table has a value at the specified key if ((TableHasKeyWithValue(tbl[i], key, value) == true) or (caseInsensitive and TableHasKeyWithValueCaseInsensitive(tbl[i], key, value) == true)) then return tbl[i] end end --for _, childTable in ipairs(tbl) do	--	if (TableHasKeyWithValue(childTable, key, value) == true) then --		return childTable --	end --end return nil end -- Returns true if the table has a key with a specific value (case sensitive) function TableHasKeyWithValue(table, key, value) return (table[key] == value) end

-- Returns true if the table has a key with a specific value (case insensitive) function TableHasKeyWithValueCaseInsensitive(table, key, value) return (string.lower(table[key]) == string.lower(value)) end

-- Returns true if the table contains a specific value, assuming the table is indexed by integer (array-like) function TableContainsValue(tbl, value) for _, v in ipairs(tbl) do       if v == value then return true end end

return false end

-- Asserts if the value of the key in the input table is nil or empty, returning true if so and erroring function TableValueIsNilOrEmpty(tbl, key) if (isNilOrEmpty(tbl[key]) == true) then error("Key \""..key.."\" in input table is nil or empty") return true else return false end end

-- String operations

function stringStartsWith(str, start) return str:sub(1, #start) == start end

function stringEndsWith(str, ending) return ending == "" or str:sub(-#ending) == ending end

-- This template will add the appropriate ordinal indicator to a given number (typically integer), returning a string containing both the number and the ordinal indicator. -- For an integer ending in 1, 2 or 3 (except for integers ending in 11, 12 or 13), the ordinal suffix will be -st, -nd and -rd, respectively. -- If the input is not a number, no ordinal indicator will be added. function stringAddOrdinal(number) local str = tostring(number) -- If the input is not a number, just return it as a string if (tonumber(number) == nil) then return str end if (stringEndsWith(str, "1") and not stringEndsWith(str, "11")) then return str.."st" elseif (stringEndsWith(str, "2") and not stringEndsWith(str, "12")) then return str.."nd" elseif (stringEndsWith(str, "3") and not stringEndsWith(str, "13")) then return str.."rd" else return str.."th" end 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: -- 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

-- Returns true if input (string) is nil or empty function isNilOrEmpty(str) return (str == nil or str == "") end

function isNotNilOrEmpty(str) return not isNilOrEmpty(str) end

-- Returns true if the input table is sequentially indexed local function isArray(t) local i = 0 for _ in pairs(t) do		i = i + 1 if t[i] == nil then return false end end return true end

-- Outputs the default if input is empty or nil function default(input, default) if (input == "" or input == nil) then return default else return input end end

-- Operates like the ternary operator in C#/JS etc function ternary(condition, valueIfTrue, valueIfFalse) if (condition == nil or condition == false)	 then return valueIfFalse else return valueIfTrue end end

return p