User File #39981160939204373

Upload All User Files

#39981160939204373 - GBA F-Zero: Maximum Velocity - de-hexing some numbers.

FZMV_BizHawk_v6a.lua
913 downloads
Uploaded 6/27/2017 1:05 PM by FatRatKnight (see all 245)
Increased the number of decimal values displayed. Not really any significant changes otherwise. As much as my habits are for me, I should realize I'm not everyone.
Also, I experienced some pretty fast crashes with BizHawk 2.0, on three different tries, with this script up. Curious.
--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")

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

  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")))  --Speed
--  gui.pixelText(  0, 21,string.format("%8d",R4s(a+0x78,"EWRAM")))  --Facing & Momentum

  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?
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
]]--