An extensive Lua HUD script for Metal Force. Mostly intended for use with glitchy behavior.
--------------------------------------------------------------------------------
-- Metal Storm (NES) --
-- Lua massive hud script - v1 --
-- By Scepheo --
--------------------------------------------------------------------------------
-- Configuration
--------------------------------------------------------------------------------
-- Whether to centre the hud on the player, regardless of camera position
local CenterOnPlayer = false
-- Whether to draw black over the level, for use with CenterOnPlayer
local ClearScreen = false
-- Hitbox colours
local PlayerColour = 0x00FF00
local EnemyColour = 0x0000FF
local EnemyProjectileColour = 0xFF0000
local FriendlyProjectileColour = 0xFFFF00
-- Exit colours
local ScrollColour = 0xFFFFFF
local BlackColour = 0xFF00FF
-- Block colours
local AirColour = 0x000000
local WallColour = 0x00AA00
local NonSolidColour = 0x444444
local LadderColour = 0xAAAA00
local FallColour = 0x00FFFF
local DamageColour = 0xFF0000
-- Drawing helper functions
--------------------------------------------------------------------------------
-- Global scaling variables
local xScale = 0
local yScale = 0
local xOffset = 0
local yOffset = 0
local xResolution = 256
local yResolution = 224
local xCam = 0
local yCam = 0
-- Recalculates scaling values for other drawing functions
function Setup()
xOffset = client.borderwidth()
yOffset = client.borderheight() - 16
xScale = client.bufferwidth() / xResolution
yScale = client.bufferheight() / yResolution
if CenterOnPlayer then
xCam = memory.read_s16_le(0x002E) - 128
yCam = memory.read_s16_le(0x0030) - 112
else
xCam = memory.read_u16_le(0x0034)
yCam = -memory.read_s16_le(0x0036)
end
if ClearScreen then
gui.drawRectangle(0, 0, 300, 300, 0xFF000000, 0xFF000000)
end
end
-- Draws a rectangle at the given in-game x/y coordinates
function DrawRectangle(x, y, width, height, color)
-- Add alpha channels to the colour
local borderColor = 0xAA000000 + color
local fillColor = 0x55000000 + color
-- Account for camera position
local dx = x - xCam
local dy = y - yCam
gui.drawRectangle(dx, dy, width, height, borderColor, fillColor)
end
-- Draws text at the given in-game x/y coordinates
-- More than one string can be given, they will be drawn under each other
function DrawText(x, y, ...)
-- Account for camera position and transform to client space
local dx = xOffset + (x - xCam) * xScale
local dy = yOffset + (y - yCam) * yScale
-- Draw all the lines under each other
for i = 1, arg.n do
gui.text(dx, dy, arg[i])
dy = dy + 12
end
end
-- Draws a hitbox at the given in-game x/y coordinates
function DrawHitbox(x, y, width, height, color)
DrawRectangle(x - width, y - height, width * 2, height * 2, color)
end
-- Block data
--------------------------------------------------------------------------------
function DrawBlocks()
local xOffset = math.floor(xCam / 16)
local yOffset = math.floor(-yCam / 16)
local drawOffsetX = xOffset * 16 - xCam
local drawOffsetY = yOffset * 16 + yCam
local width = memory.read_u8(0x0353)
local height = memory.read_u8(0x0354)
memory.usememorydomain("System Bus")
for x = 0, 16 do
local xBlock = x + xOffset
for y = 0, 15 do
local yBlock = y + yOffset
local address = bit.band(0x6000 + xBlock * height + yBlock, 0xFFFF)
local tile = memory.read_u8(address)
local colour = 0x000000
-- Air
if tile <= 0x5D then
colour = AirColour
-- Ladders
elseif tile <= 0x5F then
colour = LadderColour
-- Non-solid floors
elseif tile <= 0x6F then
colour = NonSolidColour
-- Solid floors and walls
elseif tile <= 0xE7 then
colour = WallColour
-- Falling floors
elseif tile <= 0xF3 then
colour = FallColour
-- Spikes/lava
else
colour = DamageColour
end
colour = colour + 0x66000000
local dx = x * 16 + drawOffsetX
local dy = 224 - y * 16 - drawOffsetY
gui.drawRectangle(dx, dy, 16, 16, colour, colour)
end
end
memory.usememorydomain("RAM")
end
-- Level bounds
--------------------------------------------------------------------------------
local Bounds = {}
function ReadBounds()
Bounds.Left = memory.read_s16_le(0x0355) + 14
Bounds.Bottom = -memory.read_s16_le(0x0357) + 224
Bounds.Right = memory.read_s16_le(0x0359) + 242
Bounds.Top = -memory.read_s16_le(0x035B) + 14
end
-- Transition drawing
--------------------------------------------------------------------------------
function DrawExits()
local left = ReadTransition(0x035D)
DrawExit(left, DrawLeft)
local bottom = ReadTransition(0x035F)
DrawExit(bottom, DrawBottom)
local right = ReadTransition(0x0361)
DrawExit(right, DrawRight)
local top = ReadTransition(0x0363)
DrawExit(top, DrawTop)
end
function DrawExit(transition, draw)
-- Draw scrolling transitions
while (transition.State > 0) do
draw(transition.Offset, ScrollColour)
transition.Offset = transition.Offset + 2
transition.State = transition.State - 1
end
-- Draw black transitions
while (transition.State < 0) do
draw(transition.Offset, BlackColour)
transition.Offset = transition.Offset + 2
transition.State = transition.State + 1
end
end
function DrawLeft(offset, color)
local x = Bounds.Left
local y = -memory.read_s8(0x0355 + offset) * 0x100
DrawRectangle(x - 16, y, 16, 0xFF, color)
end
function DrawBottom(offset, color)
local x = memory.read_s8(0x0355 + offset) * 0x100
local y = Bounds.Bottom
DrawRectangle(x, y, 0xFF, 16, color)
end
function DrawRight(offset, color)
local x = Bounds.Right
local y = -memory.read_s8(0x0355 + offset) * 0x100
DrawRectangle(x, y, 16, 0xFF, color)
end
function DrawTop(offset, color)
local x = memory.read_s8(0x0355 + offset) * 0x100
local y = Bounds.Top
DrawRectangle(x, y - 16, 0xFF, 16, color)
end
function ReadTransition(address)
local transition = {}
transition.State = memory.read_s8(address + 0)
transition.Offset = memory.read_u8(address + 1)
transition.Position = memory.read_u8(0x0355 + transition.Offset + 0)
transition.Index = memory.read_u8(0x0355 + transition.Offset + 1)
return transition
end
-- Enemy data
--------------------------------------------------------------------------------
function DrawEnemies()
local enemyCount = memory.read_u8(0x0467)
local dropCount = memory.read_u8(0x0468)
local totalCount = enemyCount + dropCount
local drawnCount = 0
local index = 0
if (enemyCount == 0xFF or dropCount == 0xFF) then
return
end
while (drawnCount < totalCount) do
local drawn = DrawEnemy(index)
if (drawn) then
drawnCount = drawnCount + 1
end
index = index + 1
end
end
function DrawEnemy(number)
-- Calculate memory offset
local offset = number * 16
-- We can skip the enemy drawing if it's disabled
local status = memory.read_u8(0x03A7 + offset)
if (status == 0) then
return false
end
-- Read data (16 bytes, not all know values yet)
local indexRAM = memory.read_u8(0x03A8 + offset)
local x = memory.read_s16_le(0x03AB + offset)
local y = memory.read_s16_le(0x03AD + offset)
local inv = memory.read_u8(0x03B1 + offset)
local phaseTimer = memory.read_u8(0x03B2 + offset)
local hp = memory.read_u8(0x03B4 + offset)
-- Fetch enemy table data
local drop = memory.read_u8(0x046A + indexRAM)
local indexROM = memory.read_u8(0x046B + indexRAM)
-- Read size
memory.usememorydomain("PRG ROM")
local width = memory.read_u8(0x34000 + 0x36EE + indexROM * 2)
local height = memory.read_u8(0x34000 + 0x36EF + indexROM * 2)
memory.usememorydomain("RAM")
-- Draw hitbox
DrawHitbox(x, y, width, height, EnemyColour)
local hpText = "HP: " .. hp
local invText = "INV: " .. inv
local timeText = "TIM: " .. phaseTimer
local dropText = ""
-- Only draw drop data if it drops something
if (drop > 0) then
-- If the drop is known, text, otherwise the number
if (DropTable[drop] ~= nil) then
dropText = DropTable[drop]
else
dropText = "Drop " .. drop
end
end
-- Actually draw the text
local xText = x - width + 1
local yText = y - height + 1
DrawText(xText, yText, number, hpText, invText, timeText, dropText)
return true
end
-- List of drops
DropTable =
{
[16] = "Weapon",
[17] = "1-UP",
[18] = "Health",
[19] = "Power",
[20] = "Continue"
}
-- Player data
--------------------------------------------------------------------------------
function DrawPlayer()
local x = memory.read_s16_le(0x002E)
local y = memory.read_s16_le(0x0030)
local width = memory.read_u8(0x0327)
local height = memory.read_u8(0x0328)
local state = memory.read_u8(0x0325)
local ducking = (bit.band(state, 0x08) > 0)
-- Account for ducking
if ducking then
y = y + 4
end
DrawHitbox(x, y, width, height, PlayerColour)
end
-- Enemy projectiles
--------------------------------------------------------------------------------
function DrawEnemyProjectiles()
local total = memory.read_u8(0x05BB)
local count = 0
local index = 0
if (total == 0xFF) then
return
end
while (count < total) do
local drawn = DrawEnemyProjectile(index)
if (drawn) then
count = count + 1
end
index = index + 1
end
end
function DrawEnemyProjectile(index)
local offset = index * 10
local status = memory.read_u8(0x056B + offset)
if (status == 0) then
return false
end
local x = memory.read_s16_le(0x056D + offset)
local y = memory.read_s16_le(0x056F + offset)
local sizeIndex = status * 2
-- Read size
memory.usememorydomain("PRG ROM")
local width = memory.read_u8(0x34000 + 0x375A + sizeIndex)
local height = memory.read_u8(0x34000 + 0x375B + sizeIndex)
memory.usememorydomain("RAM")
DrawHitbox(x, y, width, height, EnemyProjectileColour)
return true
end
-- Friendly projectiles
--------------------------------------------------------------------------------
function DrawFriendlyProjectiles()
local total = memory.read_u8(0x03A3)
local count = 0
local index = 0
if (total == 0xFF) then
return
end
while (count < total) do
local drawn = DrawFriendlyProjectile(index)
if (drawn) then
count = count + 1
end
index = index + 1
end
end
function DrawFriendlyProjectile(index)
local offset = index * 16
local status = memory.read_u8(0x0373 + offset)
if (status == 0) then
return false
end
local x = memory.read_s16_le(0x0375 + offset)
local y = memory.read_s16_le(0x0378 + offset)
local width = memory.read_u8(0x0329)
local height = memory.read_u8(0x032A)
DrawHitbox(x, y, width, height, FriendlyProjectileColour)
return true
end
-- Main loop
--------------------------------------------------------------------------------
while true do
Setup()
DrawBlocks()
DrawEnemies()
DrawPlayer()
DrawEnemyProjectiles()
DrawFriendlyProjectiles()
ReadBounds()
DrawExits()
-- Bounds are stored for camera, converted to character position for
-- convenience.
gui.text(150, 12, "Level Bounds")
gui.text(150, 26, "Left: " .. Bounds.Left .. "(" .. memory.read_s8(0x035D) .. ")")
gui.text(150, 40, "Right: " .. Bounds.Right .. "(" .. memory.read_s8(0x0361) .. ")")
gui.text(150, 54, "Top: " .. Bounds.Top .. "(" .. memory.read_s8(0x0363) .. ")")
gui.text(150, 68, "Bottom: " .. Bounds.Bottom .. "(" .. memory.read_s8(0x035F) .. ")")
emu.frameadvance()
end