User File #39987186376430368

Upload All User Files

#39987186376430368 - GBA F-Zero: Maximum Velocity - Red background speed hysteresis

FZMV_BizHawk_v6b.lua
922 downloads
Uploaded 6/27/2017 7:36 PM by FatRatKnight (see all 245)
Another small change. Looked at machine stats until I could identify the hysteresis flag. Speed is highlighted in red when you can't accelerate due to hysteresis (but releasing the accelerator will add coasting friction on top of maintenance friction).
Knowing speed limit would require knowing what machine you selected. Understandably, I felt finding the related flag would be less work than having the script find what you picked out.
--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 key_ToggleGhostRecord= "M"

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
}

local GhostColors= { --Try something darker, a'ite?
[0]=0xFFC0C0C0, --White   75% brightness of the main ones.
    0xFF00C000, --Green
    0xFFC0C000, --Yellow
    0xFF00C0C0, --Cyan
    0xFFC030C0  --Purple
}
local GhostEqualRadar= 0xFF606060 -- If ghost is identical, a "don't care" color.

local TrackerFile= "FZMV_PosTracker.txt" -- set nil to disable; no, not "nil".
local flag_RecordGhost= true --Overridden by false if file found.
local GhostTrailFrames= 20 --1/3 second ahead and behind

client.SetGameExtraPadding(0,0,0,120) -- Yay, bottom border


--##############################################################################
--General

--Function renames (I like descriptive names. Renames are for convenience.)
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

--Constants
local SqrtTwo= math.sqrt(2) -- In case I deal with a square or two.
local CrLf= string.char(0x0D,0x0A) --New line stuff.

--Widely important variables, as opposed to locally important
local InternalFrame= 0   -- What frame is the game itself on right now?
local PlayerSel= 4       -- Machine the player controls.

--mTracker[WhichPlayer].Stat[Frame]     Machine Tracker.
--Keeps a record of positions of every machine on every frame.
local mTracker= {}; for i= 0, 4 do mTracker[i]= {x={},y={},Exist={}} end

local function PlAddr(pl) return 0x12D60 + 0xCC*pl end     --Machine address
local function WordToAngle(v) return (v/32768)*math.pi end --cw rev/65536 to cw radians

--*****************************************************************************
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 HexPlusMinus(v,digits)
--*****************************************************************************
--String.format does not prefix a - for hexadecimal values, instead using a
--twos complement of the value. This function is to inject that sign.

  local str= "+"
  if v < 0 then str= "-"; v= -v end
  return string.format(str .. "%" .. digits .. "X",v)
end

--*****************************************************************************
local function FetchInternalFrame()
--*****************************************************************************
--I need something slightly smarter than reading just a variable.
--May want nil to indicate not in race. Want frames since race started.
--For now, it's just a memory read function.

  return R4u(0x15998,"EWRAM")
end

--*****************************************************************************
local function FetchPlayerExist(pl)
--*****************************************************************************
--I need a better method to tell if a player exists or not.
--Currently, we just compare a specific offset against -1.
--This does return a false positive, hence the desire for a better method.
--Have not identified any false negatives, which is good.

  return R1s(PlAddr(pl)+0xB6,"EWRAM") ~= -1
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= PlAddr(pl)
  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

--*****************************************************************************
local function FetchOldPos(pl,frame)
--*****************************************************************************
--Grabs stored position data. Joy.

  pl= pl or PlayerSel             --Defaults for unspecified parameters.
  frame= frame or InternalFrame
  local machine= mTracker[pl]

--nil, for never recorded. False, for recorded not existing.
  if not machine.Exist[frame] then return machine.Exist[frame] end
  return machine.x[frame], machine.y[frame]
end

--*****************************************************************************
local function RecordPos(frame)
--*****************************************************************************
--For now, it directly reads from memory and stuff it into stored position data

  frame= frame or InternalFrame  --If unspecified, assume current frame.
--Uh, now that I think about it, we're always going to assume current frame.
--Frankly, we're doing direct memory reads. This will always be current frame.
--I see a redesign in the future.

  for pl= 0, 4 do
    local a= PlAddr(pl)
    local machine= mTracker[pl]

    if FetchPlayerExist(pl) then
      machine.x[frame],machine.y[frame]= R4s(a+0x00,"EWRAM"),R4s(a+0x04,"EWRAM")
      machine.Exist[frame]= true
    else
      --leave x,y alone. If there's reason to grab stale data, I won't erase.
      machine.Exist[frame]= false
    end
  end
end

--*****************************************************************************
local function LoadTracker(filename)
--*****************************************************************************
--Exists because we can't run script, movie, then another movie.
--Well, we could, but a second movie triggers core reboot. This kills lua.
--Though, a persistent position tracker file might be useful anyway.

  local FileIn, err= io.open(filename,"r")
  if not FileIn then print("No ghost loaded"); return end
  local count= 0
  for data in FileIn:lines() do
    local frame= tonumber(string.sub(data,1,8)) --Want a decimal value.
    if frame then
      count= count+1
      for i= 0, 4 do
        local Pl= mTracker[i]
        local x= tonumber(string.sub(data,10+i*18,17+i*18)) --Decimal
        local y= tonumber(string.sub(data,19+i*18,26+i*18))
        if x and y then
          Pl.Exist[frame]= true
          Pl.x[frame],Pl.y[frame]= x,y
        else
          Pl.Exist[frame]= false
        end
      end -- for each machine
    end -- if frame exists (sanity)
  end -- Data lines

  FileIn:close()
  if count == 0 then
    print("File opened, but no frames loaded.")
  else
    flag_RecordGhost= false
    print("Ghost loaded from file. Frames: " .. count)
  end
end

--*****************************************************************************
local function SaveTracker(filename)
--*****************************************************************************
--Stuffs our tracking data into a persistent tracker file.
--I'm making my own file format on this, which is probably just plain text.

  FileOut, err= io.open(filename,"wb")
  if not FileOut then print(err); return end --Sorry, didn't save.
--Presumably, you still have the movie file, and can generate a new ghost data.

--Though, if we did succeed... Stuff it in file.
  local PlayerExisted= mTracker[PlayerSel].Exist --Shouldn't matter who, just want not nil.
  for frame,_ in pairs(PlayerExisted) do --Can't guarantee all frames exist
    local s= string.format("%8d|",frame)
    for i= 0, 4 do
      local machine= mTracker[i]
      if machine.Exist[frame] then
        s= string.format("%s%8d,%8d:",s,machine.x[frame],machine.y[frame]) --8 digits should be sufficiently large.
      else
        s= s .. "--------,--------:"
      end
    end
    FileOut:write(s .. CrLf)
    --Note that pairs won't necessarily have it in order.
  end
  FileOut:close()
  print("Ghost saved to " .. filename)
end

--##############################################################################
--Static

--*****************************************************************************
local function GetStaticDisplayLoc(sT,PosX,PosY)
--*****************************************************************************
--Apply offsets. Apply scaling. End.

  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,0xFF000080)
    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,0xFF000080)
    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 GhostTrailsStatic(sT)
--*****************************************************************************
  local ClrStep= 0xFF / GhostTrailFrames
  local Sf, Ef= InternalFrame-GhostTrailFrames, InternalFrame+GhostTrailFrames
  for pl= 0, 4 do
    for f= Sf, Ef do
      local x,y= FetchOldPos(pl,f)
      if x then
        x,y= GetStaticDisplayLoc(sT,x,y)
        if InBounds(sT,x,y) then
          local RB= (f-InternalFrame)*ClrStep
          if f < InternalFrame then --In the past
            RB= math.floor(math.abs(RB))*0x00010000
          else --In the future
            RB= math.floor(RB)
          end
          gui.drawPixel(x,y,0xFF008000+RB)
        end -- If pixel is in bounds
      end -- If machine existed
    end -- For each nearby frame
  end -- For each machine
end -- Function

--*****************************************************************************
local function GhostsStatic(sT)
--*****************************************************************************
--Shows old positions of current frame.

  for pl= 0, 4 do
    local x,y= FetchOldPos(pl)
    if x then
      local addr= PlAddr(pl)
      local mx,my= R4s(addr+0x00,"EWRAM"),R4s(addr+0x04,"EWRAM")
      local clr= GhostColors[pl]
      if (x == mx) and (y == my) then clr= GhostEqualRadar end
      x,y= GetStaticDisplayLoc(sT,x,y)
      if InBounds(sT,x,y) then
        gui.drawLine(x-3,y-3,x+3,y+3,clr)
        gui.drawLine(x-3,y+3,x+3,y-3,clr)
      end -- If pixel is in bounds
    end -- If machine existed
  end -- For each machine
end -- Function

--*****************************************************************************
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.
--I have tackled this for a while. It appears outside my capacity to debug.
--I may scrap the function and rewrite from scratch.

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

  local Tangent  = math.tan(rT.AngleF)
--  local Cotangent= math.cot(rT.AngleF)

--  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,
        rT.PlX + (Yy.v-Yy.max),
        Yy.v)
    else
      if Angle ~= 0 then
        x1,y1= GetRotateDisplayLoc(rT,
          rT.PlX,
--          rT.PlX + (Yy.v-Yy.min)*rT.Cosine/rT.Sine,
--          rT.PlX - (Yy.v-Yy.min)*rT.Cosine/rT.Sine,
--          rT.PlX + (Yy.v-Yy.min)*rT.Sine/rT.Cosine,
--          rT.PlX - (Yy.v-Yy.min)*rT.Sine/rT.Cosine,
          Yy.v)
      end
    end
    if Yy.v > Yy.right then
      if Angle ~= 0 then
        x2,y2= GetRotateDisplayLoc(rT,
          rT.PlX + 0x2000,
--          rT.PlX + (Yy.v-Yy.max)*Cosine/Sine,
          Yy.v)
      end
    else
      x2,y2= GetRotateDisplayLoc(rT,
        rT.PlX + 0x2000,
--        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.

  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)

      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

--*****************************************************************************
local function GhostTrailsRotate(rT)
--*****************************************************************************
  local ClrStep= 0xFF / GhostTrailFrames
  local Sf, Ef= InternalFrame-GhostTrailFrames, InternalFrame+GhostTrailFrames
  for pl= 0, 4 do
    for f= Sf, Ef do
      local x,y= FetchOldPos(pl,f)
      if x then
        x,y= GetRotateDisplayLoc(rT,x,y)
        if InBounds(rT,x,y) then
          local RB= (f-InternalFrame)*ClrStep
          if f < InternalFrame then --In the past
            RB= math.floor(math.abs(RB))*0x00010000
          else --In the future
            RB= math.floor(RB)
          end
          gui.drawPixel(x,y,0xFF008000+RB)
        end -- If pixel is in bounds
      end -- If machine existed
    end -- For each nearby frame
  end -- For each machine
end -- Function

--*****************************************************************************
local function GhostsRotate(rT)
--*****************************************************************************
--Shows old positions of current frame.

  for pl= 0, 4 do
    local x,y= FetchOldPos(pl)
    if x then
      local addr= PlAddr(pl)
      local mx,my= R4s(addr+0x00,"EWRAM"),R4s(addr+0x04,"EWRAM")
      local clr= GhostColors[pl]
      if (x == mx) and (y == my) then clr= GhostEqualRadar end
      x,y= GetRotateDisplayLoc(rT,x,y)
      if InBounds(rT,x,y) then
        gui.drawLine(x-3,y-3,x+3,y+3,clr)
        gui.drawLine(x-3,y+3,x+3,y-3,clr)
      end -- If pixel is in bounds
    end -- If machine existed
  end -- For each machine
end -- Function

--*****************************************************************************
local function PlayerGhostRotate(rT)
--*****************************************************************************
--You are your own worst enemy. If you can beat yourself, overcome anything!
--Just don't beat yourself up on this.

  local x,y= FetchGhost(InternalFrame)
  if not x then return end
  x,y= GetRotateDisplayLoc(rT,x,y)
  if InBounds(rT,x,y) then --Draw ye X, rather than +.
    gui.drawLine(x-3,y-3,x+3,y+3,0xFFC0C0C0)
    gui.drawLine(x-3,y+3,x+3,y-3,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_hex(n)
--*****************************************************************************
--Pick a machine, show its stats.
--So long as there is a vague reason to do so, things are in hexadecimal.
--Otherwise, decimal.

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

  local Hysteresis= bit.band(R1u(a+0xC8,"EWRAM"),0x10) ~= 0
  local clr= nil
  if Hysteresis then clr= 0x80FF2000 end

  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")),0xFFFFFFFF,clr)  --Speed

  x= x - R4s(a+0x08,"EWRAM")
  y= y - R4s(a+0x0C,"EWRAM")
  gui.pixelText( 33,  0,string.format("%4X",math.abs(x)),ClrBySign(x))
  gui.pixelText( 33,  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

--Facing, momentum, and their difference.
  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))

--Elevation
  local Height,VertVel= R4s(a+0x54,"EWRAM"),R2s(a+0x84,"EWRAM")
--  gui.pixelText(207, 21,string.format("%8d",Height))
--  gui.pixelText(207, 28,string.format("%8d",VertVel))
--Acceleration: -12 per frame. -16 if not holding down after some point.
--I assume it's always -12, so you know the farthest you can go.
  if Height > 0 then
    v= math.ceil((VertVel + math.sqrt(VertVel*VertVel + 4*6*Height))/12)
  else v= 0
  end
  gui.pixelText(224, 14,string.format("%4d",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, 21,string.format("%4d",R1u(a+0x9E,"EWRAM")))  --Split

  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 function MachineHUD_dec(n)
--*****************************************************************************
--Pick a machine, show its stats.
--No hexadecimal here. It's all decimal.
--Don't recommend viewing angles like this.

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

  local Hysteresis= bit.band(R1u(a+0xC8,"EWRAM"),0x10) ~= 0
  local clr= nil
  if Hysteresis then clr= 0x80FF2000 end

  gui.pixelText(  0,  0,string.format("%8d",x))
  gui.pixelText(  0,  7,string.format("%8d",y))
  gui.pixelText(  0, 16,string.format("%8d",R2s(a+0x74,"EWRAM")),0xFFFFFFFF,clr)  --Speed

  x= x - R4s(a+0x08,"EWRAM")
  y= y - R4s(a+0x0C,"EWRAM")
  gui.pixelText( 33,  0,string.format("%+5d",x),ClrBySign(x))
  gui.pixelText( 33,  7,string.format("%+5d",y),ClrBySign(y))
  local v= math.floor(math.sqrt(x*x + y*y)) -- Distance formula
  gui.pixelText(  0, 23,string.format("%8d",v),0xFF00FFFF) -- Change in position

--Facing, momentum, and their difference.
  gui.pixelText(  0,160,string.format("%5d",Facing)  ,0xFFFF00FF)
  gui.pixelText(  0,167,string.format("%5d",Momentum),0xFF00FF00)
  v= (Facing - Momentum + 0x8000)%0x10000 - 0x8000
  gui.pixelText(  0,174,string.format("%5d",math.abs(v)),ClrBySign(v))

--Elevation
  local Height,VertVel= R4s(a+0x54,"EWRAM"),R2s(a+0x84,"EWRAM")
--  gui.pixelText(207, 21,string.format("%8d",Height))
--  gui.pixelText(207, 28,string.format("%8d",VertVel))
--Acceleration: -12 per frame. -16 if not holding down after some point.
--I assume it's always -12, so you know the farthest you can go.
  if Height > 0 then
    v= math.ceil((VertVel + math.sqrt(VertVel*VertVel + 4*6*Height))/12)
  else v= 0
  end
  gui.pixelText(224, 14,string.format("%4d",v))

  gui.pixelText(220,  0,string.format("%5d",R2u(a+0x8A,"EWRAM")))  --Pow
  gui.pixelText(224,  7,string.format("%4d",R1u(a+0xA2,"EWRAM")))  --Lap seg
--  gui.pixelText(224, 21,string.format("%4d",R1u(a+0x9E,"EWRAM")))  --Split

  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)
  GhostTrailsStatic(sT)
  GhostsStatic(sT)
  RivalRadarNorth(sT)
--  PlayerTrailS(sT)

end

--*****************************************************************************
local function RotatingHUD(rT)
--*****************************************************************************
-- Oriented so player facing is toward the top.
-- There's a lot of duplication between static and rotated.
-- Done this way so I get a better feel on how to remove duplication.
-- Just haven't spent time on the removal yet, but I have ideas.

--  GridUnderlayR(rT)
  MomentumAngle(rT)
  GhostTrailsRotate(rT)
  GhostsRotate(rT)
  RivalRadarFacing(rT)
--  PlayerTrailR(rT)

end


--Immediate
local StatsTbl= {}
if TrackerFile then
  LoadTracker(TrackerFile)
  event.onexit(function() SaveTracker(TrackerFile) end)
end

--*****************************************************************************
while true do
--*****************************************************************************
--Our overhead.

  InternalFrame= R4u(0x15998,"EWRAM")

  local keyboard= input.get()
  if keyboard[key_ToggleGhostRecord] then flag_RecordGhost= not flag_RecordGhost end
  PlayerSel= R1u(0x2B63,"IWRAM")
  if PlayerSel < 5 then
    FillTable(       StatsTbl,StaticX,StaticY,StaticR,StaticS,PlayerSel)
    StaticHUD(StatsTbl)
    PartialFillTable(StatsTbl,RotateX,RotateY,RotateR,RotateS)
    RotatingHUD(StatsTbl)
    MachineHUD_dec(PlayerSel)
  end
  if flag_RecordGhost then 
    RecordPos()
    gui.pixelText(120,160,"REC",0xFFFF2000)
  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)

08360B08
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)
  +4C,4s - Dist traveled?
  +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?)
  +9E,1u - Which split did you take on the track?
  +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?
  +C8,1x - Bitpacked:
    ...x....: Over speed limit, hysteresis in effect
EWRAM:131CF,1u - ? Player machine selection related?
EWRAM:15998,4u - Frame count
EWRAM:1599C,4u - Frame count

ROM:049CCC[size=0x40][count=7] - ?
ROM:04EA58 - ?

ROM:2C1CC0[size=0x30][count=25] - Machine core stats
  +00,1u[4] - Acceleration (for each gear)
  +04,2u[4] - Top speed for each gear
  +0C,1u    - Friction for being over speed limit of 4th gear (boost maintenance)
  +0D,1x    - Padding?
  +0E,2u    - Speed to drop to from over 4th gear limit (hysteresis)
  +10,1u    - Boost Acceleration
  +11,1x    - Padding?
  +12,2u    - Boost top speed
  +14,1u    - Friction for being over boost speed limit
  +15,1x    - Padding?
  +16,2u    - Speed to drop to from over boost limit (boost hysteresis)
  +18,2u    - Boost Time
  +1A,2u    - Jump
  +1C,2u    - ?
  +1E,1u    - Friction: Coasting (accelerator released)
  +1F,1u    - Friction: Braking
  +20,2u    - Steering rate
  +22,2u    - Momentum rate (balance)
  +24,2u    - Momentum rate (Blast Turn)
  +26,2u    - ?
  +28,2u    - ?
  +2A,2u    - LR drift speed
  +2C,1u    - ?
  +2D,1u    - ?
  +2E,1u    - Body
  +2F,1u    - ?
FB - -B
ST - -3
]]--