From CloudModding MM Wiki

This module is used to implement a template that generates address boxes

Input

Header

  • space - Denotes what "space" the address box will use for reference. supported spaces are offset, ram, rom, manual, the global context and certain files (at the moment, only code (File). Makes it simpler to document both ram and rom addresses with a single reference
  • name - Sets address box's caption, or overrides the default caption if the space is set to offset, ram, or rom
  • subpage - Sets super page for the address table, and adds a link to each row to the relevant subpage for that version.
  • hideoff - Optional. Hides the offset column if set to true. Default value is false
  • hideend - Optional. Hides the end column(s) if set to true. Default value is false

Records

  • ver# - Sets version name. Only a specific set of values will be accepted here
  • ref# - Sets what type of reference is being recorded. Values are offset, ram, rom, and manual.

If ref# is set to offset, ram, or rom, you can use the following parameters:

  • start# - Sets the start address or offset for the reference
  • end# - Optional. Sets the end address or offset for the reference
  • size# - Optional. Sets the size spanned by the reference

Otherwise, when ref# is set to manual, you can set the following parameters.

  • offset# - Sets the start offset for the reference
  • ram-start# - sets the ram address start for the reference
  • ram-end#
  • rom-start#
  • rom-end#

Note that offset# can be omitted if hideoff = true. Similarly, if space = ram, rom-start#/end# can be omitted, and if space = rom, ram-start#/end# can be omitted.


local p = {}
local origArgs
local args = {}
local root

local yesno = require('Module:Yesno')
local versions = 
{
	["Invalid"]={ k=-1, n="Invalid Version Code"},
	["debug"]={ k=1, n="Debug" },
	["j 1.0"]={ k=2, n="J 1.0" },
	["j 1.1"]={ k=3, n="J 1.1" },
	["u 1.0"]={ k=4, n="U 1.0" },
	["ntsc-j 1.0"]={ k=2, n="J 1.0" },
	["ntsc-j 1.1"]={ k=3, n="J 1.1" },
	["ntsc-u"]={ k=4, n="U 1.0" },
	["pal 1.0"]={ k=5, n="PAL 1.0" },
	["pal 1.1"]={ k=6, n="PAL 1.1" },
	["jp gc"]={ k=7, n="JP GC" },
	["usa gc"]={ k=8, n="USA GC" },
	["pal gc"]={ k=9, n="PAL GC" },
	["demo"]={ k=10, n="Demo" }
}

local function GetVersionInfo(key) -- key is the string key used to look up a table value
	local t = versions[key:lower()]
    if t == nil then
    	t = versions["Invalid"] 
    end
    return t
end

local data = {
	ram = {
		display="[[VRam]]", ram=true, rom=false, offset=false 
		},
	rom = {
		display="[[VRom]]", ram=false, rom=true, offset=false
		},
	off = {
		display="Offset", ram=false, rom=false, offset=true
		},
	manual = {
		display="<Set Display>", ram=true, rom=true, offset=true
		},
	code = {
		display="[[Code (File)]]", ram=true, rom=true, offset=true,
		["Debug"] = { 
			rom = { 0xC95000, 0xE12600 },
			ram = { 0x800B6AC0, 0x802340C0 }, 
			},
		["J 1.0"] = {
			rom = { 0xB5F000, 0xC9B6F0 },
			ram = { 0x800A76A0, 0x801E3D90 },
			}, 
		["J 1.1"] = {
			rom = { 0xB5F000, 0xC9BA60 },
			ram = { 0x800A75E0, 0x801E4040 },
			},
		["U 1.0"] = {
			rom = { 0xB3C000, 0xC7A4E0 },
			ram = { 0x800A5AC0, 0x801E3FA0 },
			},
		["PAL 1.0"] = {
			rom = { 0xC8A000, 0xDBF730 },
			ram = { 0x800A5D60, 0x801DB490 },
			},
		["PAL 1.1"] = {
			rom = { 0xC8A000, 0xDBF850 },
			ram = { 0x800A5FE0, 0x801DB830 },
			},
		["JP GC"] = {
			rom = { 0xB45000, 0xC80D40 },
			ram = { 0x800A6420, 0x801E2160 },
			},
		["USA GC"] = {
			rom = { 0xB4B000, 0xC86D30 },
			ram = { 0x800A6440, 0x801E2170 },
			},
		["PAL GC"] = {
			rom = { 0xC99000, 0xDCC820 },
			ram = { 0x800A65A0, 0x801D9DC0 },
			},
		},
	ovl_player_actor = {
		display="[[ovl_player_actor|Link Actor]]", ram=true, rom=true, offset=true,
		["Debug"] = {
			rom = { 0xE46460, 0xE7FF20 },
			ram = { 0x80833ED0, 0x8086DA20 },
			},
		["J 1.0"] = {
			rom = { 0xCC9A20, 0xCFE660 },
			ram = { 0x8082E390, 0x808630F0 },
			},
		["U 1.0"] = {
			rom = { 0xCA7F00, 0xCDCF60 },
			ram = { 0x8082DA90, 0x80862B70 },
			},
		}, 
	ovl_kaleido_scope={ 
		display="[[ovl_kaleido_scope|Pause Screen]]", ram=true, rom=true, offset=true,
		["Debug"] = {
			rom = { 0xE2C6A0, 0xE46460 },
			ram = { 0x8081A0D0, 0x80833ED0 },
			},
		["J 1.0"] = {
			rom = { 0xCB14B0, 0xCC9A20 },
			ram = { 0x80815DE0, 0x8082E390 },
			},
		["U 1.0"] = {
			rom = { 0xC90550, 0xCA7F00 },
			ram = { 0x808160A0, 0x8082DA90 },
			},
		},
	gctxt = 
	{ 
		display="[[Global Context]]", ram=true, rom=false, offset=true,
		["Debug"] = { ram = { 0x80448700, 0x80448700+0x019268 }, }, 
		["J 1.0"] = 	{ ram = { 0x803E6CF0, 0x803E6CF0+0x019238 }, }, 
		["U 1.0"] = { ram = {0x803E6B20, 0x803E6B20+0x19258}, }, 
		}, 
	}

local function getRowOrder()
	-- Returns a table containing the numbers of the arguments that exist
    -- for the specified prefix. For example, if the prefix was 'data', and
    -- 'data1', 'data2', and 'data5' exist, it would return {1, 2, 5}.
    local prefix = 'ver'
    local tbl = {}
    local keys = {}
    local nums = {}
    for k, v in pairs(args) do --keys, values in arguments
        local num = tostring(k):match('^' .. prefix .. '([1-9]%d*)$') --get param group the version is bound to
        if num then
        	local verId = GetVersionInfo(args[k]).k
        	table.insert(keys, verId);
        	tbl[verId] = tonumber(num) --key is sort order, value is row
        	end
    end
    
    table.sort(keys)
    for _, k in ipairs(keys) do 
    	table.insert(nums, tbl[k])
		end
		
    return nums
end

local function preprocessSingleArg(argName)
    -- If the argument exists and isn't blank, add it to the argument table.
    -- Blank arguments are treated as nil to match the behaviour of ParserFunctions.
    if origArgs[argName] and origArgs[argName] ~= '' then
        args[argName] = origArgs[argName]
    end
end

local function preprocessArgs(prefixTable, step)
    -- Assign the parameters with the given prefixes to the args table, in order, in batches
    -- of the step size specified. This is to prevent references etc. from appearing in the
    -- wrong order. The prefixTable should be an array containing tables, each of which has
    -- two possible fields, a "prefix" string and a "depend" table. The function always parses
    -- parameters containing the "prefix" string, but only parses parameters in the "depend"
    -- table if the prefix parameter is present and non-blank.
    if type(prefixTable) ~= 'table' then
        error("Non-table value detected for the prefix table", 2)
    end
    if type(step) ~= 'number' then
        error("Invalid step value detected", 2)
    end
    
    -- Get arguments without a number suffix, and check for bad input.
    for i,v in ipairs(prefixTable) do
        if type(v) ~= 'table' or type(v.prefix) ~= "string" or (v.depend and type(v.depend) ~= 'table') then
            error('Invalid input detected to preprocessArgs prefix table', 2)
        end
        preprocessSingleArg(v.prefix)
        -- Only parse the depend parameter if the prefix parameter is present and not blank.
        if args[v.prefix] and v.depend then
            for j, dependValue in ipairs(v.depend) do
                if type(dependValue) ~= 'string' then
                    error('Invalid "depend" parameter value detected in preprocessArgs')
                end
                preprocessSingleArg(dependValue)
            end
        end
    end

    -- Get arguments with number suffixes.
    local a = 1 -- Counter variable.
    local moreArgumentsExist = true
    while moreArgumentsExist == true do
        moreArgumentsExist = false
        for i = a, a + step - 1 do
            for j,v in ipairs(prefixTable) do
                local prefixArgName = v.prefix .. tostring(i)
                if origArgs[prefixArgName] then
                    moreArgumentsExist = true -- Do another loop if any arguments are found, even blank ones.
                    preprocessSingleArg(prefixArgName)
                end
                -- Process the depend table if the prefix argument is present and not blank, or
                -- we are processing "prefix1" and "prefix" is present and not blank, and
                -- if the depend table is present.
                if v.depend and (args[prefixArgName] or (i == 1 and args[v.prefix])) then
                    for j,dependValue in ipairs(v.depend) do
                        local dependArgName = dependValue .. tostring(i)
                        preprocessSingleArg(dependArgName)
                    end
                end
            end
        end
        a = a + step
    end
end

local function renderTableTop(spaceRef)
	
	root = mw.html.create('table')
	root
		:attr('id', 'Addr')
		:addClass('wikitable')
		:attr('align', 'right')
	local caption = root :tag('caption')
	
	if args.name then
		if args.space=='ram' or args.space=='rom' or args.space=='manual' then
			caption:wikitext(args.name)
			else
			caption :wikitext(spaceRef.display .. '<br>' .. args.name)
		end
	else
		caption :wikitext(spaceRef.display)
	end
	
	local header = root:tag('tr')
	
	header
		:tag('th')
		:wikitext('Version')
	
	if spaceRef.offset == true and args.hideoff ~= true then
		header
			:tag('th')
			:wikitext('Offset')
	end
	local colspan = 2
	if args.hideend == true then
		colspan = 1
	end
	
	if spaceRef.rom then
		header
			:tag('th')
			:attr('colspan',colspan)
			:wikitext('[[VRom]]')
	end
	
	if spaceRef.ram then
		header
			:tag('th')
			:attr('colspan',colspan)
			:wikitext('[[VRam]]')
	end
		
		
end

local function renderDataCell(row, value, f)
	f = f or '%08X'
	row :tag('td'):css('font-size', '90%'):tag('tt'):wikitext(string.format(f, value))
end

local function renderDataGroup(row, d)
	-- renders cells with a shared header column (i.e. VRam/VRom)
	if d.Start < 0 then
		renderDataCell(row, "?", "%s")
	else
		renderDataCell(row, d.Start)
	end

	if args.hideend ~= true then
		if d.End < d.Start or d.End < 0 then 
			renderDataCell(row, "?", "%s") 
		else 
			renderDataCell(row, d.End) 
		end
	end
end

local function renderRow(ver, off, rom, ram)

	if args.subpage then
		ver = "[[" .. args.subpage .."/" .. ver .. "|" .. ver .. "]]"
	end
	local row = root:tag('tr')
	row :tag('td'):wikitext(ver)
	
	if off.Show and args.hideoff ~= true then
		if off.Start < 0 then
			renderDataCell(row, '?', '%s')
		else
			renderDataCell(row, off.Start, '%06X')
		end
	end
	
	if rom.Show then
		renderDataGroup(row, rom)
	end
	
	if ram.Show then
		renderDataGroup(row, ram)
	end
	
	return result
end


local function processRowManual(spaceRef, rowArgs)
	if not rowArgs.ver then return	end
	
	rowArgs.offset = rowArgs.offset == nil and -1 or tonumber(rowArgs.offset,16)
	rowArgs['rom-start'] = rowArgs['rom-start'] == nil and -1 or tonumber(rowArgs['rom-start'],16)
	rowArgs['rom-end'] = rowArgs['rom-end'] == nil and -1 or tonumber(rowArgs['rom-end'],16)
	rowArgs['ram-start'] = rowArgs['ram-start'] == nil and -1 or tonumber(rowArgs['ram-start'],16)
	rowArgs['ram-end'] = rowArgs['ram-end'] == nil and -1 or tonumber(rowArgs['ram-end'],16)
	
	
	local off = { Show=spaceRef.offset, Start=rowArgs.offset, End=-1 }
	local rom = { Show=spaceRef.rom, Start=rowArgs['rom-start'], End=rowArgs['rom-end'] }
	local ram = { Show=spaceRef.ram, Start=rowArgs['ram-start'], End=rowArgs['ram-end'] }
	
	renderRow(rowArgs.ver, off,rom,ram)
end

local function processRow(spaceRef, rowArgs)
	if not rowArgs.ver or not rowArgs.start then return	end
	if not rowArgs.ref then
		rowArgs.ref = 'off'	
	elseif rowArgs.ref == 'offset' then
		rowArgs.ref = 'off'
	end
	
	local verAddr = spaceRef[rowArgs.ver] -- can be nil
	
	local off = { Show=spaceRef.offset, Start=-1, End=-1 }
	local rom = { Show=spaceRef.rom, Start=-1, End=-1 }
	local ram = { Show=spaceRef.ram, Start=-1, End=-1 }
	
	rowArgs.start = tonumber(rowArgs.start,16) %0x80000000
	if rowArgs['end'] then
		rowArgs['end'] = tonumber(rowArgs['end'],16) %0x80000000
	end
	if rowArgs.size then
		rowArgs.size = tonumber(rowArgs.size,16)
	end
	
	if not rowArgs['end'] and rowArgs.size then
		rowArgs['end'] = rowArgs.start + rowArgs.size
	end
	if not rowArgs['end'] then
		rowArgs['end'] = -1 -- avoids proper error checking
	end
	
	-- Calculate data
	
	if rowArgs.ref == 'off' then
		off.Start = rowArgs.start
		off.End = rowArgs['end']
		if verAddr and ram.Show then
			ram.Start = verAddr.ram[1] + off.Start
			ram.End = verAddr.ram[1] + off.End
		end
		if verAddr and rom.Show then
			rom.Start = verAddr.rom[1]+off.Start
			rom.End = verAddr.rom[1]+off.End
		end
	
	elseif rowArgs.ref == 'ram' then
		ram.Start = rowArgs.start + 0x80000000
		ram.End = rowArgs['end'] + 0x80000000
		
		if verAddr and off.Show then
			off.Start = ram.Start - verAddr.ram[1]
			off.End = ram.End - verAddr.ram[1]
		end
		
		if verAddr and rom.Show then
			rom.Start = verAddr.rom[1]+off.Start
			rom.End = verAddr.rom[1]+off.End
		end
		
	elseif rowArgs.ref == 'rom' then
		rom.Start = rowArgs.start
		rom.End = rowArgs['end'] 
		
		if verAddr and off.Show then
			off.Start = rom.Start - verAddr.rom[1]
			off.End = rom.End - verAddr.rom[1]
		end
		if verAddr and ram.Show then
			ram.Start = verAddr.ram[1] + off.Start
			ram.End = verAddr.ram[1] + off.End
		end 
	end
	renderRow(rowArgs.ver, off,rom,ram)
end


local function renderRows(space)
	local rownums = getRowOrder()
	for k, num in ipairs(rownums) do 
		local ref = args['ref' .. tostring(num)]
		local version = GetVersionInfo(args['ver' .. tostring(num)]).n
		if ref and ref == 'manual' then
			processRowManual(space,{
				ver = version,
				offset = args['offset' .. tostring(num)],
				['rom-start'] = args['rom-start' .. tostring(num)],
				['rom-end'] = args['rom-end' .. tostring(num)],
				['ram-start'] = args['ram-start' .. tostring(num)],
				['ram-end'] = args['ram-end' .. tostring(num)],
				})
		else
			processRow(space,{
				ver = version,
				ref = args['ref' .. tostring(num)],
				start = args['start' .. tostring(num)],
				['end'] = args['end' .. tostring(num)],
				size = args['size' .. tostring(num)],
				})
		end
	end
end

local function _address()
	local space
	
	if args.space then
		space = data[args.space]
	else return "<Error: no space set>" end
	
	renderTableTop(space)
	renderRows(space)

	return tostring(root)
end

-- performs auto-magic calculations by referencing a database
function p.address(frame)
    -- If called via #invoke, use the args passed into the invoking template.
    -- Otherwise, for testing purposes, assume args are being passed directly in.
    if frame == mw.getCurrentFrame() then
        origArgs = frame:getParent().args
    else
        origArgs = frame
    end
    
    preprocessSingleArg('space')
    preprocessSingleArg('name')
    preprocessSingleArg('hideoff')
    preprocessSingleArg('hideend')
    preprocessSingleArg('subpage')
    preprocessArgs({
    	{ prefix = 'ver'},
    	{ prefix = 'ref'},
    	{ prefix = 'start'},
    	{ prefix = 'end'},
    	{ prefix = 'size'},
    	
    	-- For manual declaration of addresses
    	{ prefix = 'offset' },
    	{ prefix = 'rom-start'},
    	{ prefix = 'rom-end'},
    	{ prefix = 'ram-start'},
    	{ prefix = 'ram-end'},
    	
    },10)
    
    args.hideoff = args.hideoff == nil and false or yesno(args.hideoff)
    args.hideend = args.hideend == nil and false or yesno(args.hideend)
    
    return _address()
    
end

-- Genenerates a table of supported version codes
function p.versions(frame)
	
	function compare (a,b) return a[1] < b[1] end
	local troot = mw.html.create('table')
	troot
		:addClass('wikitable')
		:addClass('mw-collapsible')
		:addClass('mw-collapsed')
		
	local caption = troot:tag('caption')
	caption:wikitext("Supported Versions")
	
	local header = troot:tag('tr') 
	header
		:tag('th')
		:wikitext('Code')
		:tag('th')
		:wikitext('Version')
		
	local t = {}
	for k, v in pairs(versions) do
		table.insert(t, {v.k, k, v.n})
	end
	
	table.sort(t, compare)
	for k, v in ipairs(t) do
		local row = troot:tag('tr')
		row:tag('td')
		:wikitext(v[2])
		:tag('td')
		:wikitext(v[3])
	end
	return tostring(troot)
end

return p