--GBA F-Zero: Maximum Velocity - General script
--Lower-left is the north-oriented display
--Lower-right is the facing-oriented display
--For use with BizHawk
--FatRatKnight
--Setup
local StaticX,StaticY= 60,220 --Center position
local StaticR= 60 --Radius
local StaticS= 0x0400 --Scale
local RotateX,RotateY= 180,220 --Center position
local RotateR= 60 --Radius
local RotateS= 0x0200 --Scale
local RadarColors= {
[0]=0xFFFFFFFF, --White Note, the player can be any of first four.
0xFF00FF00, --Green Depends on which spot the machine starts in.
0xFFFFFF00, --Yellow
0xFF00FFFF, --Cyan I advise bright colors and distinct hues.
0xFFFF40FF --Purple
}
client.SetGameExtraPadding(0,0,0,120) -- Yay, bottom border
--##############################################################################
--General
local R4u , R4s= memory.read_u32_le , memory.read_s32_le
local R2u , R2s= memory.read_u16_le , memory.read_s16_le
local R1u , R1s= memory.read_u8 , memory.read_s8
local SqrtTwo= math.sqrt(2) -- A constant, so I avoid recalculating it.
--*****************************************************************************
local function FetchAddrDomainGBA(a)
--*****************************************************************************
--Stand-in for System Bus. Highly desired when you got a full pointer.
--I don't know all regions, though.
if (a >= 0x02000000) and (a < (0x02000000+memory.getmemorydomainsize("EWRAM"))) then
return a-0x02000000, "EWRAM"
elseif (a >= 0x03000000) and (a < (0x03000000+memory.getmemorydomainsize("IWRAM"))) then
return a-0x03000000, "IWRAM"
elseif (a >= 0x08000000) and (a < (0x08000000+memory.getmemorydomainsize("ROM"))) then
return a-0x08000000, "ROM"
else
error(string.format("Unknown address %08X", a),1)
end
end
--*****************************************************************************
local function WordToAngle(v) --revision 3
--*****************************************************************************
-- Input: Angle in 1/65536 of a revolution per unit, clockwise, no offset
-- Output: Angle in radians, clockwise, no offset
-- Well, Y increases as it goes down, so leave things like that.
return (v/32768)*math.pi
end
--*****************************************************************************
local function PartialFillTable(T,x,y,r,s)
--*****************************************************************************
--Exists mainly to relocate or rescale drawing area without re-fetching stats.
T.x= x; T.y= y; T.r= r; T.s= s
T.Left= x-r; T.Top= y-r; T.Right= x+r; T.Bottom= y+r
end
--*****************************************************************************
local function FillTable(T,x,y,r,s,pl)
--*****************************************************************************
--This exists so I only have to do one calculation for multiple uses.
--I pay in table dereferencing, though.
T= T or {} --construct, in case we were fed nil as first parameter
--Most of these won't change frame by frame. Possibly wasteful to retry.
T.x= x; T.y= y; T.r= r; T.s= s; T.pl= pl
T.Left= x-r; T.Top= y-r; T.Right= x+r; T.Bottom= y+r
--Player stats...
local a= 0x12D60 + pl*0xCC --"EWRAM", can't I use 0x02012D60 and be done?
T.Addr= a --Address, in case there are special stats I did not get here.
T.PlX= R4s(a+0x00,"EWRAM") --Player X
T.PlY= R4s(a+0x04,"EWRAM") --Player Y
local Facing= R2u(a+0x78,"EWRAM")
T.Facing= Facing
Facing= WordToAngle(Facing) --Convert to mathematical angle
T.AngleF= Facing
T.Sine= math.sin(Facing); T.Cosine= math.cos(Facing)
return T --If we were fed the table, the caller doesn't need to handle this
end
--*****************************************************************************
local function InBounds(T,x,y)
--*****************************************************************************
--Returns true or false, generally for drawing area.
return (x >= T.Left) and (x <= T.Right) and (y >= T.Top) and (y <= T.Bottom)
end
--##############################################################################
--Static
--*****************************************************************************
local function GetStaticDisplayLoc(sT,PosX,PosY)
--*****************************************************************************
--
local x= math.floor((PosX - sT.PlX)/sT.s+0.5) + sT.x
local y= math.floor((PosY - sT.PlY)/sT.s+0.5) + sT.y
return x,y
end
--*****************************************************************************
local function GridUnderlayS(sT)
--*****************************************************************************
--Might be nice to have a dark colored map underneath the radar.
--For now, have these grid lines.
local range= (sT.r+0.5) * sT.s
--Vertical lines
local Vline= (math.ceil((sT.PlX - range)/0x4000)*0x4000 - sT.PlX) / sT.s + sT.x
while Vline <= sT.Right do
gui.drawLine(Vline,sT.Top,Vline,sT.Bottom,0xFF404040)
Vline= Vline + 0x4000/sT.s
end
--Horizontal lines
local Hline= (math.ceil((sT.PlY - range)/0x4000)*0x4000 - sT.PlY) / sT.s + sT.y
while Hline <= sT.Bottom do
gui.drawLine(sT.Left,Hline,sT.Right,Hline,0xFF404040)
Hline= Hline + 0x4000/sT.s
end
end
--*****************************************************************************
local function MomentumCompass(sT)
--*****************************************************************************
--Yay, compass! In case you're lost! ... Somehow?
--Facing first. So its line is painted under the momentum line.
-- local z= sT.AngleF
local z= WordToAngle(R2s(sT.Addr+0x78,"EWRAM"))
local x= sT.x + sT.r*math.cos(z)
local y= sT.y + sT.r*math.sin(z)
gui.drawLine(sT.x, sT.y,x,y,0xFFFF00FF)
--Momentum second.
z= WordToAngle(R2s(sT.Addr+0x7A,"EWRAM")) --Momentum
x= sT.x + sT.r*math.cos(z)
y= sT.y + sT.r*math.sin(z)
gui.drawLine(sT.x, sT.y,x,y,0xFF00FF00)
end
--*****************************************************************************
local function RivalRadarNorth(sT)
--*****************************************************************************
--Watches for rivals around.
--It is oriented northward, by the way.
local OriginX,OriginY= sT.PlX,sT.PlY
for i= 0, 4 do --We will paint the player as a side-effect here.
local a= 0x12D60 + i*0xCC
local MachineX,MachineY= R4s(a+0,"EWRAM"),R4s(a+4,"EWRAM")
if (R1s(a+0xB6,"EWRAM") ~= -1) then
local X= math.floor((MachineX-OriginX)/sT.s+0.5) + sT.x
local Y= math.floor((MachineY-OriginY)/sT.s+0.5) + sT.y
if InBounds(sT , X,Y) then
local clr= RadarColors[i] or 0xFFC0C0C0 --Fallback shouldn't happen...
gui.drawLine(X-4,Y ,X+4,Y ,clr)
gui.drawLine(X ,Y-4,X ,Y+4,clr)
end
end
end
end
--*****************************************************************************
local function PlayerTrailS(sT)
--*****************************************************************************
--Well, the game keeps a short list of old positions. Let's display them!
for i= 0, 3 do
local x= R4s(sT.Addr+0x10 + 8*i,"EWRAM")
local y= R4s(sT.Addr+0x14 + 8*i,"EWRAM")
x,y= GetStaticDisplayLoc(sT,x,y)
gui.drawPixel(x,y,0xFFC0C0C0)
end
end
--#############################################################################
--Rotating
--*****************************************************************************
local function GetRotateDisplayLoc(rT,PosX,PosY)
--*****************************************************************************
--Rotatey stuff.
local x= PosX - rT.PlX
local y= PosY - rT.PlY
x,y= -rT.Sine*x+rT.Cosine*y, -rT.Cosine*x-rT.Sine*y
x= math.floor(x/rT.s+0.5) + rT.x
y= math.floor(y/rT.s+0.5) + rT.y
return x,y
end
--*****************************************************************************
local function GridUnderlayR(rT)
--*****************************************************************************
--Rotated underlay. Now that should be a fun exercise in trig.
--Incomplete function. I'm seriously out of practice in my math, and am not
--getting the Y lines to behave. Do not use this function.
--Get our triangle sides
local Angle= WordToAngle(rT.Facing%0x4000 - 0x2000) --45 degree offset
local Hypotinuse= rT.r * rT.s * SqrtTwo --Radius, scale, to corner of square
local LongSide= math.cos(Angle) * Hypotinuse
local ShortSide= math.sin(Angle) * Hypotinuse
local Xx= {v= math.ceil((rT.PlX - LongSide)/0x4000)*0x4000, min= rT.PlX - LongSide, max= rT.PlX + LongSide, left= rT.PlX - ShortSide, right= rT.PlX + ShortSide}
local Yy= {v= math.ceil((rT.PlY - LongSide)/0x4000)*0x4000, min= rT.PlY - LongSide, max= rT.PlY + LongSide, left= rT.PlY - ShortSide, right= rT.PlY + ShortSide}
Angle= WordToAngle(rT.Facing%4000) -- Don't need the diagonal now
local Sine= math.sin(Angle)
local Cosine= math.cos(Angle)
-- while x < MaxX do
-- Xx.v= Xx.v + 0x4000
-- end
while Yy.v < Yy.max do
local x1,y1 , x2,y2
if Yy.v > Yy.left then
x1,y1= GetRotateDisplayLoc(rT,
rT.PlX - (Yy.v-Yy.min)*Cosine/Sine,
Yy.v)
else
if Angle ~= 0 then
x1,y1= GetRotateDisplayLoc(rT,
-- rT.PlX + (Yy.v-Yy.max)*Sine/Cosine,
rT.PlX - (Yy.v-Yy.max)*Sine/Cosine,
-- rT.PlX + (Yy.v-Yy.min)*Sine/Cosine,
-- rT.PlX - (Yy.v-Yy.min)*Sine/Cosine,
-- rT.PlX + (Yy.v-Yy.max)*Cosine/Sine,
-- rT.PlX - (Yy.v-Yy.max)*Cosine/Sine,
-- rT.PlX + (Yy.v-Yy.min)*Cosine/Sine,
-- rT.PlX - (Yy.v-Yy.min)*Cosine/Sine,
Yy.v)
end
end
if Yy.v > Yy.right then
if Angle ~= 0 then
x2,y2= GetRotateDisplayLoc(rT,
rT.PlX + (Yy.v-Yy.max)*Cosine/Sine,
Yy.v)
end
else
x2,y2= GetRotateDisplayLoc(rT,
rT.PlX + (Yy.v-Yy.min)*Sine/Cosine,
Yy.v)
end
if x1 and x2 then gui.drawLine(x1,y1,x2,y2,0xFF404040) end
Yy.v= Yy.v + 0x4000
end
end
--*****************************************************************************
local function MomentumAngle(rT)
--*****************************************************************************
--Always facing forward, so omit the facing line. Only our momentum line counts
--Might as well note north, though.
local a= rT.Addr
local Facing= R2u(a+0x78,"EWRAM")
local Momentum= R2u(a+0x7A,"EWRAM")
local Diff= (Facing - Momentum + 0x8000)%0x10000 - 0x8000
--North
local Angle= WordToAngle(Facing)
local HalfR= rT.r/2
local x= rT.x - HalfR*math.cos(Angle)
local y= rT.y + HalfR*math.sin(Angle)
gui.drawLine(x-1,y ,x+1,y ,0xFF808080)
gui.drawLine(x ,y-1,x ,y+1,0xFF808080)
--Momentum, relative to facing
Angle= WordToAngle(Diff)
x= rT.x - rT.r*math.sin(Angle)
y= rT.y - rT.r*math.cos(Angle)
gui.drawLine(rT.x,rT.y,x,y,0xFF00FF00)
end
--*****************************************************************************
local function RivalRadarFacing(rT)
--*****************************************************************************
--The rival watch.
--Oriented based on player's machine facing.
local OriginX,OriginY= rT.PlX, rT.PlY
local Sine= rT.Sine
local Cosine= rT.Cosine
for i= 0, 4 do --We will paint the player as a side-effect here.
local a= 0x12D60 + i*0xCC
local MachineX,MachineY= R4s(a+0,"EWRAM"),R4s(a+4,"EWRAM")
if (R1s(a+0xB6,"EWRAM") ~= -1) then
local X,Y= GetRotateDisplayLoc(rT,MachineX,MachineY)
-- local X,Y= MachineX-OriginX,MachineY-OriginY
-- X,Y= -Sine*X+Cosine*Y, -Cosine*X-Sine*Y
-- X= math.floor(X/rT.s+0.5) + rT.x
-- Y= math.floor(Y/rT.s+0.5) + rT.y
if InBounds(rT , X,Y) then
local clr= RadarColors[i] or 0xFFC0C0C0 --Fallback shouldn't happen...
gui.drawLine(X-4,Y ,X+4,Y ,clr)
gui.drawLine(X ,Y-4,X ,Y+4,clr)
end
end
end
end
--*****************************************************************************
local function PlayerTrailR(rT)
--*****************************************************************************
--Rotating things around for the player's trail.
for i= 0, 3 do
local x= R4s(rT.Addr+0x10 + 8*i,"EWRAM")
local y= R4s(rT.Addr+0x14 + 8*i,"EWRAM")
x,y= GetRotateDisplayLoc(rT,x,y)
gui.drawPixel(x,y,0xFFC0C0C0)
end
end
--#############################################################################
--Misc display
--*****************************************************************************
local function ClrBySign(v)
--*****************************************************************************
if v < 0 then return 0xFFFFFF00 end
if v > 0 then return 0xFF00FFFF end
return 0xFFFF00FF
end
--*****************************************************************************
local function MachineHUD(n)
--*****************************************************************************
local a= 0x12D60 + n*0xCC
local x, y= R4s(a+0x00,"EWRAM"), R4s(a+0x04,"EWRAM")
local Facing, Momentum= R2u(a+0x78,"EWRAM"), R2u(a+0x7A,"EWRAM")
gui.pixelText( 0, 0,string.format("%8X",x))
gui.pixelText( 0, 7,string.format("%8X",y))
gui.pixelText( 0, 16,string.format("%8X",R2s(a+0x74,"EWRAM"))) --Speed
-- gui.pixelText( 0, 21,string.format("%8X",R4s(a+0x78,"EWRAM"))) --Facing & Momentum
x= x - R4s(a+0x08,"EWRAM")
y= y - R4s(a+0x0C,"EWRAM")
gui.pixelText( 32, 0,string.format("%4X",math.abs(x)),ClrBySign(x))
gui.pixelText( 32, 7,string.format("%4X",math.abs(y)),ClrBySign(y))
local v= math.floor(math.sqrt(x*x + y*y)) -- Distance formula
gui.pixelText( 0, 23,string.format("%8X",v),0xFF00FFFF) -- Change in position
gui.pixelText( 0,160,string.format("%4X",Facing) ,0xFFFF00FF)
gui.pixelText( 0,167,string.format("%4X",Momentum),0xFF00FF00)
v= (Facing - Momentum + 0x8000)%0x10000 - 0x8000
gui.pixelText( 0,174,string.format("%4X",math.abs(v)),ClrBySign(v))
gui.pixelText(224, 0,string.format("%4X",R2u(a+0x8A,"EWRAM"))) --Pow
gui.pixelText(224, 7,string.format("%4d",R1u(a+0xA2,"EWRAM"))) --Lap seg
gui.pixelText(224,153,string.format("%4d",R2u(a+0x8C,"EWRAM"))) --Boost timer
gui.pixelText(224,145,string.format("%4d",R1u(a+0xA1,"EWRAM"))) --Trigger timer
end
local ImportantMachineBackClr= {[21]=0x400000FF,[22]=0x400000FF,[23]=0x60FFFFFF}
--*****************************************************************************
local function BasicHUD()
--*****************************************************************************
--Generally for basic calculations and all that.
--Also a scratch field for various tests.
for i= 0, 4 do
local addr= 0x12D60 + 0xCC*i
local v= R1s(addr+0xB6,"EWRAM")
local machine= R1s(addr+0xB0,"EWRAM")
local clr= RadarColors[i]
if v == -1 then clr= 0xFFA0A0A0 end
local bclr= ImportantMachineBackClr[machine]
-- gui.pixelText(231,160+7*i,string.format("%2d",v),clr)
gui.pixelText(227,160+7*i,string.format("%3d",R1u(addr+0xA2,"EWRAM")),clr,bclr)
end
--[[
for x= 0, 4 do
for y= 0, 22 do
local addr= 0x12D60 + 0xCC*x + 4*y + 0x80
gui.pixelText(36*x,7*y,string.format("%08X",R4u(addr,"EWRAM")),RadarColors[x])
end
end
]]--
-- for i= 0, 4 do
-- local addr= 0x12D60 + 0xCC*x + 4*y + 0x80
-- end
end
--#############################################################################
--Management
--*****************************************************************************
local function StaticHUD(sT)
--*****************************************************************************
-- Oriented so north is toward the top. Fun stuff.
GridUnderlayS(sT)
MomentumCompass(sT)
RivalRadarNorth(sT)
PlayerTrailS(sT)
end
--*****************************************************************************
local function RotatingHUD(rT)
--*****************************************************************************
-- Oriented so player facing is toward the top.
-- GridUnderlayR(rT)
MomentumAngle(rT)
RivalRadarFacing(rT)
PlayerTrailR(rT)
end
--Immediate
local StatsTbl= {}
--*****************************************************************************
while true do
--*****************************************************************************
--Our overhead.
local Player= R1u(0x2B63,"IWRAM")
if Player < 5 then
FillTable( StatsTbl,StaticX,StaticY,StaticR,StaticS,Player)
StaticHUD(StatsTbl)
PartialFillTable(StatsTbl,RotateX,RotateY,RotateR,RotateS)
RotatingHUD(StatsTbl)
MachineHUD(Player)
end
BasicHUD()
emu.frameadvance()
end
--#############################################################################
--eof. Well, extra data on hand below.
--[[
IWRAM:106C,4u - Timer (?)
IWRAM:2B62,1u - Player machine ID
IWRAM:2B63,1u - Player machine memory internal position
EWRAM:0A100,1x[Count=0x4000?] Input log history (4.5 minutes)
EWRAM:0E560,2x[x=64][y=64] An array of track block index
EWRAM:10560,?
12D60 12E2C 12EF8 12FC4 13090
EWRAM:12D60[Size=0xCC][Count=5] Machine data
+00,4s - X position (main)
+04,4s - Y position (main)
+08,4s - X position (1 frame ago)
+0C,4s - Y position (1 frame ago)
+10,4s - X position (1 frame ago)
+14,4s - Y position (1 frame ago)
+18,4s - X position (2 frames ago)
+1C,4s - Y position (2 frames ago)
+20,4s - X position (3 frames ago)
+24,4s - Y position (3 frames ago)
+28,4s - X position (4 frames ago)
+2C,4s - Y position (4 frames ago)
+54,4s - Elevation
+74,2s - Speed
+78,2x - Facing
+7A,2x - Momentum
+84,2s - Vertical velocity
+8A,2u - Power
+8C,2u - Boost timer
+8E,2u - Boost timer (mirror)
+94,4x - Apparent health (for that visual health meter?)
+9F,1u - ? Internal reference ID?
+A1,1u - Timer for holding down boost
+A2,1u - Lap segment
+B0,1x - Machine identifer (what it is; 23 is a mine)
+B6,1s - ID?
EWRAM:131CF,1u - ? Player machine selection related?
]]--