Module:SongData

local public = {} local private = {} local json = require("Module:JSON")

-- Load song data and pack order local titleobject = mw.title.new("User:Jono99/Song Data") local song_data = json.decode(titleobject:getContent) titleobject = mw.title.new("User:Jono99/Pack Order") local pack_order = json.decode(titleobject:getContent)

-- Removes leading and trailing spaces from strings function private.trimWhitespace(str) local _start = 1 while (str:sub(_start, _start) == " ") do       _start = _start + 1 end local _end = str:len while (str:sub(_end, _end) == " ") do       _end = _end - 1 end return str:sub(_start, _end) end

-- Checks if a table has a specific key function private.hasKey(_table, key) return _table[key] ~= nil end

-- Gets a specific item from a table, with a fallback if the table doesn't have that key function private.getValue(_table, key, fallback) if private.hasKey(_table, key) then return _table[key] else return fallback end end

-- Converts artist string/table into string or link to that artist's section in Artists function private.getArtist(get, linkable) local artist = get if type(artist) == "table" then artist = get["format"] for i,v in ipairs(get) do           local artist_alias1, artist_alias2 = artist:find("<<<" .. i .. "|.->>>") if artist_alias1 then if linkable then artist = artist:sub(0, artist_alias1 - 1) .. "Artists" .. artist:sub(artist_alias2 + 1) else artist = artist:sub(0, artist_alias1 - 1) .. artist:sub(artist_alias1 + 4 + tostring(i):len, artist_alias2 - 3) .. artist:sub(artist_alias2 + 1) end else local replace = v               if linkable then replace = "" .. v .. "" end artist = artist:gsub("<<<" .. i .. ">>>", replace) end end elseif linkable then artist = "" .. artist .. "" end return artist end

function private.compareSongs(a, b)   if a["version"] == b["version"] then return a["position"] < b["position"] end return a["version"] < b["version"] end

function private.getTableRaw(_table, args) local pos = _table local no_key_response = "ERROR: This table doesn't have the key " for _, v in ipairs(args) do       local v = private.trimWhitespace(v) no_key_response = no_key_response .. '[' .. v .. ']'       if private.hasKey(pos, v) then pos = pos[v] else return no_key_response end end if type(pos) == "table" then local retval = "ERROR: the requested value is a table. Keys:" for k, v in pairs(pos) do           retval = retval .. " - " .. k .. ": " .. tostring(v) end return retval else return pos end end

-- Debug function to make sure the process of loading the song data is working properly function public.getSongData return song_data end

-- Returns the full name of a pack, if it exists function public.getPackName(pack_id) if private.hasKey(pack_order, pack_id) then local pack = pack_order[pack_id] local retval = "" if private.hasKey(pack, "subtitle") then retval = pack["subtitle"] .. " - "       end retval = retval .. private.getValue(pack, "name", "") return retval end return nil end

-- Strips the decimal point off a number function public.truncateDecimal(num) local _type = type(num) if _type == "table" then return public.truncateDecimal(tonumber(private.trimWhitespace(num["args"][1]))) elseif _type == "number" then return math.floor(num) else return num end end

-- Returns an instance of the Song template, with all information filled in, taken from the Song Data JSON function public.Song(frame) if private.hasKey(frame.args, 1) then local song_key = private.trimWhitespace(frame.args[1]) if private.hasKey(song_data, song_key) then -- Get song data from JSON and build template arguments local song_template = "Song" local song_data = song_data[song_key] local tcp = {} -- Template call parameters tcp["title"] = song_data["title"] tcp["title_latin"] = song_data["title_latin"] tcp["image"] = song_data["image"] tcp["pack"] = public.getPackName(song_data["pack"]) tcp["cost"] = song_data["cost"] tcp["source"] = song_data["source"] if private.hasKey(song_data, "artist") then tcp["compound_artist"] = private.getArtist(song_data["artist"], true) end tcp["bpm"] = song_data["bpm"] if type(tcp["bpm"]) == "table" then local bpm = tcp["bpm"]["min"] if (bpm ~= tcp["bpm"]["max"]) then bpm = tostring(bpm) .. "~" .. tostring(tcp["bpm"]["max"]) end bpm = tostring(bpm) if private.getValue(tcp["bpm"], "unstable", false) then bpm = bpm .. "?"           	end tcp["bpm"] = bpm end tcp["duration"] = song_data["duration"] tcp["charter"] = song_data["charter"] tcp["illustration"] = song_data["illustration"] if private.hasKey(song_data, "unlock") then tcp["unlock_hint"] = song_data["unlock"]["hint"] tcp["unlock_method"] = song_data["unlock"]["method"] end if private.hasKey(song_data, "difficulties") then local difficulties = "" for i, d in ipairs(song_data["difficulties"]) do local difficulty = private.getValue(d, "name", "?") .. "," .. private.getValue(d, "level", "?") .. "," .. private.getValue(d, "notes", "???") if private.hasKey(d, "charter") then difficulty = difficulty .. ",c:" .. d["charter"] end if private.hasKey(d, "rating") then difficulty = difficulty .. ",r:" .. d["rating"] end if i > 1 then difficulties = difficulties .. ";"           		end difficulties = difficulties .. difficulty end tcp["difficulties"] = difficulties end return frame:expandTemplate{title = song_template, args = tcp} else return " ERROR: Song data does not contain a song with id \"" .. song_key .. "\" " .. public.listSongs end else return " ERROR: No song id provided " .. public.listSongs end end

-- Returns a wikitable listing all the songs in a pack, with all songs listed and information filled in, taken from the Song Data and Pack Order JSONs function public.SongPagePackListing(frame) if private.hasKey(frame.args, 1) then local pack_key = private.trimWhitespace(frame.args[1]) if pack_key == "single" then return public.SongPageSinglePackListing elseif private.hasKey(pack_order, pack_key) then local pack_list = pack_order[pack_key] local table_rows = {} local diffs = {} local diffs_inverse = {} -- Used for checking if a difficulty is in diffs for i, v in ipairs(pack_list["songs"]) do           	local song = song_data[v] if private.getValue(song, "display", true) and private.hasKey(song, "difficulties") then for _, d in ipairs(song["difficulties"]) do           			if not private.hasKey(diffs_inverse, d["name"]) then table.insert(diffs, d["name"]) diffs_inverse[d["name"]] = true end end end end for i, v in ipairs(pack_list["songs"]) do               local row = mw.html.create("tr") local song = song_data[v] local difficulty_columns = mw.html.create("tr") if private.getValue(song, "display", true) then if private.hasKey(song, "difficulties") then for i, v in ipairs(diffs) do               			local diff = {} for _, d in ipairs(song["difficulties"]) do               				if d["name"] == v then diff = d               				end end local node = mw.html.create("td") node :css("width", tostring(100.0 / #diffs) .. "%") :css("text-align", "center") :css("border-bottom", "0px") if i == 1 or i == #diffs then node :css("border-left", "0px") :css("border-right", "0px") end node:wikitext(private.getValue(diff, "level", "-")) :allDone difficulty_columns:node(node) end else difficulty_columns :tag("td") :wikitext("DIFFICULTIES") :done end difficulty_columns:allDone row :tag("td") :wikitext("" .. private.getValue(song, "title", v) .. "") :done :tag("td") :wikitext(private.getArtist(private.getValue(song, "artist", "?"), false)) :done :tag("td") :css("padding", "0px") :css("border", "0px") :tag("table") :addClass("wikitable") :css("width", "100%") :css("margin", "0px") :css("border", "0px") :node(difficulty_columns) :done :done row:allDone table.insert(table_rows, tostring(row)) end end local retval = mw.html.create("table") local header_diff_columns = mw.html.create("tr") for i, v in ipairs(diffs) do           	local node = mw.html.create("th") node :css("width", tostring(100.0 / #diffs) .. "%") if i == 1 or i == #diffs then node:css("border", "0px") else node :css("border-top", "0px") :css("border-bottom", "0px") end node:wikitext(v) :allDone header_diff_columns:node(node) end retval :addClass("wikitable") :css("width", "100%") :tag("tr") :tag("th") :css("width", "46%") :wikitext("Song") :done :tag("th") :css("width", "30%") :wikitext("Artist") :done :tag("th") :css("width", "24%") :css("padding", "0px") :css("border", "0px") :tag("table") :addClass("wikitable") :css("width", "100%") :css("margin", "0px") :css("border", "0px") :node(header_diff_columns) :done :done :done for _, v in ipairs(table_rows) do               retval:node(v) end retval:allDone return tostring(retval) else return " ERROR: Pack order does not contain a pack with name \"" .. pack_key .. "\" " .. public.listPacksFromPO end else return " ERROR: No song pack provided " .. public.listPacksFromPO end end

-- Returns a wikitable listing all the songs in the Single Collection, with all songs listed and information filled in, taken from the Song Data and Pack Order JSONs function public.SongPageSinglePackListing local pack_list = pack_order["single"] local table_rows = {} local diffs = {} local diffs_inverse = {} -- Used for checking if a difficulty is in diffs for i, v in ipairs(pack_list["songs"]) do   	local song = song_data[v] if private.getValue(song, "display", true) and private.hasKey(song, "difficulties") then for _, d in ipairs(song["difficulties"]) do   			if not private.hasKey(diffs_inverse, d["name"]) then table.insert(diffs, d["name"]) diffs_inverse[d["name"]] = true end end end end for i, v in ipairs(pack_list["songs"]) do       local row = mw.html.create("tr") local song = song_data[v] local difficulty_columns = mw.html.create("tr") if private.getValue(song, "display", true) then if private.hasKey(song, "difficulties") then for i, v in ipairs(diffs) do       			local diff = {} for _, d in ipairs(song["difficulties"]) do       				if d["name"] == v then diff = d       				end end local node = mw.html.create("td") node :css("width", tostring(100.0 / #diffs) .. "%") :css("text-align", "center") :css("border-bottom", "0px") if i == 1 or i == #diffs then node :css("border-left", "0px") :css("border-right", "0px") end node:wikitext(private.getValue(diff, "level", "-")) :allDone difficulty_columns:node(node) end else difficulty_columns :tag("td") :wikitext("DIFFICULTIES") :done end difficulty_columns:allDone row :tag("td") :wikitext("" .. private.getValue(song, "title", v) .. "") :done :tag("td") :wikitext(private.getArtist(private.getValue(song, "artist", "?"), false)) :done :tag("td") :wikitext(private.getValue(song, "version", "?.??")) :done :tag("td") :css("padding", "0px") :css("border", "0px") :tag("table") :addClass("wikitable") :css("width", "100%") :css("margin", "0px") :css("border", "0px") :node(difficulty_columns) :done :done row:allDone table.insert(table_rows, tostring(row)) end end local retval = mw.html.create("table") local header_diff_columns = mw.html.create("tr") for i, v in ipairs(diffs) do   	local node = mw.html.create("th") node :css("width", tostring(100.0 / #diffs) .. "%") if i == 1 or i == #diffs then node:css("border", "0px") else node :css("border-top", "0px") :css("border-bottom", "0px") end node:wikitext(v) :allDone header_diff_columns:node(node) end retval :addClass("wikitable") :css("width", "100%") :tag("tr") :tag("th") :css("width", "40%") :wikitext("Song") :done :tag("th") :css("width", "30%") :wikitext("Artist") :done :tag("th") :css("width", "6%") :wikitext("Version") :done :tag("th") :css("width", "24%") :css("padding", "0px") :css("border", "0px") :tag("table") :addClass("wikitable") :css("width", "100%") :css("margin", "0px") :css("border", "0px") :node(header_diff_columns) :done :done :done for _, v in ipairs(table_rows) do       retval:node(v) end retval:allDone return tostring(retval) end

-- Returns the number of normal songs in the game, taken from the Song Data JSON function public.SongCount local song_count = 0 for k, v in pairs(song_data) do       if v["gateway"] == nil and private.getValue(v, "display", true) then song_count = song_count + 1 end end return song_count end

-- Returns a table of all songs in the game, grouped into packs, sorted primarily by release version, then by pack order. Takes information from the Song Data and Pack Order JSONs. function public.SongListTable local final = mw.html.create("table") final :css("width", "100%") :addClass("wikitable") :addClass("mw-collapsible") :addClass("mw-collapsed") :attr("align", "center") :tag("tr") :tag("th") :attr("colspan", "2") :wikitext("Songs") :done :done for _, p in ipairs(pack_order["__order"]) do       if private.hasKey(pack_order, p) then local pack = pack_order[p] local row_string = "" local draw_row = false if table.getn(private.getValue(pack, "songs", {})) > 0 then local songs = {} for i, s in ipairs(pack["songs"]) do                   if private.getValue(private.getValue(song_data, s, {["display"] = false}), "display", true) then draw_row = true local song = song_data[s] song["key"] = s                       song["position"] = i                        table.insert(songs, song) end end table.sort(songs, private.compareSongs) for i, s in ipairs(songs) do                   if i > 1 then row_string = row_string .. " &bull; " end row_string = row_string .. "" .. private.getValue(s, "title", s["key"]) .. "" end end if draw_row then final :tag("tr") :tag("th") :css("width", "23%") :wikitext("" .. pack["name"] .. "") :done :tag("td") :css("width", "77%") :wikitext(row_string) :done :done end end end final:allDone return tostring(final) end

-- Returns a table of all songs written by a specific artist, taken from the Song Data JSON function public.ArtistSongs(frame) local args = frame:getParent.args if private.hasKey(args, 1) then -- Get songs local songs = {} for _, p in ipairs(pack_order["__order"]) do           for _, s in ipairs(pack_order[p]["songs"]) do                songs[#songs + 1] = s            end end if false then return songs end local artist = private.trimWhitespace(args[1]) local artist_songs = {} -- Get all songs by artist for i, k in pairs(songs) do           local v = song_data[k] local by_artist = false if private.getValue(v, "display", true) then if private.hasKey(v, "artist") then if type(v["artist"]) == "string" then by_artist = v["artist"] == artist else for _, s in ipairs(v["artist"]) do                           if s == artist then by_artist = true end end end if by_artist then v["key"] = k                       v["position"] = i                        table.insert(artist_songs, v)                    end end end end table.sort(artist_songs, private.compareSongs) -- Add additional songs if private.hasKey(args, "force_insert") then for sp in args["force_insert"]:gmatch("([^#]+)") do               local s = nil local p = nil for i in sp:gmatch("([^,]+)") do                   if s == nil then s = i                   else p = i                   end end local song = song_data[s] song["key"] = s               local position = nil if p == "start" then position = 1 else for i, k in ipairs(artist_songs) do                       if k["key"] == p then position = i                           break end end end table.insert(artist_songs, position, song) end end local final = mw.html.create("table") final :addClass("article-table") :css("width", "100%") :tag("tr") :tag("th") :css("width", "30%") :wikitext("Song") :done :tag("th") :css("width", "30%") :wikitext("Pack") :done :tag("th") :css("width", "40%") :wikitext("Notes") :done :done for _, v in ipairs(artist_songs) do           local song = v            final :tag("tr") :tag("td") :wikitext("" .. private.getValue(song, "title", v["key"]) .. "") :done :tag("td") :wikitext(public.getPackName(song["pack"])) :done :tag("td") :wikitext(private.getValue(args, v["key"], "")) :done :done end final:allDone return tostring(final) else return " ERROR: No artist provided " end end

-- Returns a specific piece of data from the Song Data JSON function public.GetSDRaw(frame) return private.getTableRaw(song_data, frame.args) end

-- Returns a specific piece of data from the Pack Order JSON function public.GetPORaw(frame) return private.getTableRaw(pack_order, frame.args) end

-- Lists all the keys in song_data function public.listSongs local normal_songs = "=== Normal ===" local hidden_songs = "=== Hidden ===" for k, v in pairs(song_data) do       local song_title = k        if private.hasKey(v, "gateway") then hidden_songs = hidden_songs .. " " .. song_title else normal_songs = normal_songs .. " " .. song_title end end local final = "== List of songs == " .. normal_songs .. " " .. hidden_songs local retval = mw.html.create("div") retval :addClass("mw-collapsible") :wikitext(final) return tostring(retval) end

-- Lists all the packs used in songs in song_data. Used to check if any packs are misspelt. function public.listPacksFromSD local packs = {} for k, v in pairs(song_data) do       if packs[v["pack"]] == nil then packs[v["pack"]] = true end end local final = "== Packs ==" for pack in pairs(packs) do       final = final .. " " .. pack end local retval = mw.html.create("div") retval :addClass("mw-collapsible") :wikitext(final) return tostring(retval) end

function public.listPacksFromPO local packs = {} for k, v in pairs(pack_order) do       table.insert(packs, k)    end local final = "== Packs ==" for _, pack in ipairs(packs) do       final = final .. " " .. pack end local retval = mw.html.create("div") retval :addClass("mw-collapsible") :wikitext(final) return tostring(retval) end

-- Lists all the artists that aren't mentioned in Artists function public.listUndocumentedArtists local sanitised_characters = {"%", "(", ")", ".", "+", "-", "*", "?", "[", "^", "$"} local titleobject = mw.title.new("Artists") local artists_page = titleobject:getContent local undocumented_artists = {} -- Get all artists for k, v in pairs(song_data) do       if private.hasKey(v, "artist") then local artist = v["artist"] if type(artist) == "string" then undocumented_artists[artist] = k           elseif type(artist) == "table" then for i, v2 in ipairs(artist) do                   undocumented_artists[v2] = k                end end end end -- Remove mentioned artists for artist in pairs(undocumented_artists) do       local artist_sanitised = artist for _, chr in ipairs(sanitised_characters) do           artist_sanitised = artist_sanitised:gsub("%" .. chr, "%%" .. chr) end if artist_sanitised:sub(-1) == "%" then return artist_sanitised end if string.find(artists_page, "{{%s*ArtistSongs%s*|%s*" .. artist_sanitised .."%s*[|}]") ~= nil then undocumented_artists[artist] = nil end end -- Return result local retval = mw.html.create("span") for k, v in pairs(undocumented_artists) do       retval :wikitext("\"" .. k .. "\" - " .. v .. "") :tag("br") :done end retval:allDone return tostring(retval) end

function public.listCharacters local cd_page = mw.title.new("User:Jono99/Character Data") local character_data = json.decode(cd_page:getContent) local retval = "" for i, character in ipairs(character_data) do		if private.hasKey(character, "name") then local character_paragraph = "\n=== " .. character["name"] .. " ===" .. "\nVersion: " if private.hasKey(character, "version") then local version = character["version"] if type(version) == "table" then character_paragraph = character_paragraph .. "Added in " .. private.getValue(version, "added", "Unknown") .. ", removed in " .. private.getValue(version, "removed", "Unknown") else character_paragraph = character_paragraph .. tostring(version) end else character_paragraph = character_paragraph .. "Unknown" end character_paragraph = character_paragraph .. " \nUnlock method: " if private.getValue(character, "cost", 0) == 0 then character_paragraph = character_paragraph .. "None" else character_paragraph = character_paragraph .. "Purchase for " .. tostring(character["cost"]) .. " credits" end character_paragraph = character_paragraph .. " \nInfo: " .. private.getValue(character, "info", "???") character_paragraph = character_paragraph .. " \nMessages:\n" for mi, message in ipairs(private.getValue(character, "dialogue", {})) do character_paragraph = character_paragraph .. "* " .. message .. "\n" end character_paragraph = character_paragraph .. " \nArtist: " .. private.getValue(character, "illustration", "???") retval = retval .. character_paragraph end end return retval end return public