Guest User

info and ghost display script for TASing in DeSmuME

a guest
Feb 4th, 2011
588
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. -- info and ghost display script for TASing in DeSmuME
  2. -- supported games: Sonic Rush, Sonic Rush Adventure, Sonic Colo[u]rs, and New Super Mario Bros.
  3. -- original script author: nitsuja
  4.  
  5. -- script input configuration
  6. hide_key = 'capslock'
  7. protect_ghost_key = 'leftclick'
  8. delete_ghost_key = 'rightclick'
  9. adjust_ghost_backward_key = 'leftbracket'
  10. adjust_ghost_forward_key = 'rightbracket'
  11. -- note: press forward and backward keys together -> reset adjustment
  12.  
  13. -- script feature set settings
  14. draw_info_text = true
  15. draw_velocity_trails = true
  16. draw_ghosts = true
  17. max_ghosts = 16
  18. color_coded_speed = true
  19. use_in_game_time = true
  20. keep_history_across_sessions = true
  21.  
  22.  
  23.  
  24.  
  25.  
  26.  
  27.  
  28. if keep_history_across_sessions then
  29.   -- define which variable live across multiple runs of the script
  30.   emu.persistglobalvariables{
  31.     currenttracknumber = 1,
  32.  
  33.     -- a record of frame information...
  34.     -- looks like: {savenumber1={time1=x1y1,time2=x2y2,...},savenumber2=...}
  35.     frameinforecords = {}, -- initially it's an empty table
  36.   }
  37. else
  38.   currenttracknumber = 1
  39.   frameinforecords = {}
  40. end
  41.  
  42.  
  43.  
  44. local drawinfo_y = 30
  45. local drawinfo = function(msg,val,xoff,xoff2,color)
  46.   gui.text(180+(xoff or 0),drawinfo_y,msg,0xffffff3f)
  47.   gui.text(220+(xoff2 or 0),drawinfo_y,val or _G[msg],color)
  48.   drawinfo_y = drawinfo_y + 10
  49. end
  50.  
  51.  
  52.  
  53. -- a helper function to convert from unpacked x,y position to combined xy
  54. local function packxy(x,y)
  55.   return OR(x, SHIFT(y, -16))
  56. end
  57. -- a helper function to convert from packed xy position to separate x,y
  58. local function unpackxy(xy)
  59.   local x = AND(xy, 0xFFFF)
  60.   local y = SHIFT(xy, 16)
  61.   if y > 32767 then y = y - 65536 end
  62.   return x,y
  63. end
  64.  
  65.  
  66.  
  67. -- hack fix for desmume's readlongunsigned returning signed values
  68. local origreadlongunsigned = memory.readlongunsigned
  69. memory.readlongunsigned = function(...)
  70.   rv = origreadlongunsigned(...)
  71.   if rv < 0 then rv = 1 + (rv + 0xFFFFFFFF) end
  72.   return rv
  73. end
  74.  
  75.  
  76. -- hack fix for new bug in desmume 0.9.7 with integers > 0x7FFFFFFF getting mangled when read in by the emulator
  77. local function fixcolor(c) return type(c)=='number' and OR(c,0) or c end
  78. local origdrawtext,origdrawline,origdrawbox = gui.text,gui.drawline,gui.drawbox
  79. gui.text = function(x,y,s,c1,c2) return origdrawtext(x,y,s,fixcolor(c1),fixcolor(c2)) end
  80. gui.drawline = function(x1,y1,x2,y2,c1,s) return origdrawline(x1,y1,x2,y2,fixcolor(c1),s) end
  81. gui.drawbox = function(x1,y1,x2,y2,c1,c2) return origdrawbox(x1,y1,x2,y2,fixcolor(c1),fixcolor(c2)) end
  82.  
  83.  
  84.  
  85.  
  86. -- stub out game-specific functions until we know which game this is
  87. function getcamx() return 0 end
  88. function getcamy() return 0 end
  89. function istopontop() return true end
  90. function isnotinlevel() return false end
  91. function getinstantid() return 0 end
  92. function getperframeinfo() return 0 end
  93.  
  94. function initgamespecificfunctions()
  95.  
  96.   if whichgame == 'sc' then
  97.  
  98.     function getcamx()
  99.       -- estimates the camera position of 2 frames ago, since that matches up best visually
  100.       local x = memory.readlong(0x021203BC+8+ramshift)*2 - memory.readlong(0x021203BC+ramshift)
  101.       return math.floor((x / 4096) + .5)
  102.     end
  103.     function getcamy()
  104.       -- each screen has its own set of camera values, which will be different for the y component.
  105.       -- this function returns the y value of whichever screen is on top.
  106.       local y1 = memory.readlong(0x021203C0+8+ramshift)*2 - memory.readlong(0x021203C0+ramshift)
  107.       local y2 = memory.readlong(0x02120448+8+ramshift)*2 - memory.readlong(0x02120448+ramshift)
  108.       return math.floor((math.min(y1,y2) / 4096) + .5)
  109.     end
  110.     function istopontop()
  111.       return memory.readlong(0x021203C0+ramshift) < memory.readlong(0x02120448+ramshift)
  112.     end
  113.     function isnotinlevel()
  114.       return memory.readlong(0x02163EE4+ramshift) == 0 or memory.readword(0x0212089C+ramshift) == 0 -- hack
  115.     end
  116.     function getinstantid()
  117.       -- this function should return whatever metric we want to minimize,
  118.       -- although it should be scaled such that it most often increases by 1 per frame.
  119.       -- for example, we could return (framecount + estimated_future_delays) or (framecount - score*n).
  120.       -- in this case, let's simply use in-game level time and nothing else.
  121.       return memory.readword(0x0212089C+ramshift) -- time =
  122.     end
  123.     function getperframeinfo()
  124.       -- a helper function to get current player position info
  125.       -- (packed into a single number, since we might store a LOT of this data...)
  126.  
  127.       --local baseaddr = memory.readlong(0x02120850+ramshift) -- often 0x0219B220
  128.       --local rawx = memory.readlong(baseaddr + 0x44)
  129.       --local x = rawx/4096
  130.       --local y = memory.readlong(baseaddr + 0x48)/4096
  131.       --local lagx = memory.readlong(0x27E36EC+ramshift)
  132.       --if lagx < 32768 and lagx ~= rawx then -- hack to smooth out near-lag frames
  133.       --  -- (a better way is sync the script to memory.registerwrite of the level timer,
  134.       --  -- but non-dev desmume doesn't support that function)
  135.       --  x = x + memory.readlong(baseaddr + 0xC0)/4096
  136.       --  y = y + memory.readlong(baseaddr + 0xC4)/4096
  137.       --end
  138.        
  139.       -- more 'correct' values. the other way makes for obviously screwy-looking trails.
  140.       -- unfortunately this way is 1 frame behind, but that might be unavoidable because
  141.       -- this could be the earliest frame that the game is actually sure about the position.
  142.       local x = memory.readlong(0x0212A340+ramshift)/4096
  143.       local y = memory.readlong(0x0212A344+ramshift)/4096
  144.       return packxy(x,y)
  145.     end
  146.  
  147.   elseif whichgame == 'sra' then
  148.  
  149.     function getcamx()
  150.       local x = memory.readlong(0x02133ACC+8+ramshift3)*2 - memory.readlong(0x02133ACC+ramshift3)
  151.       return math.floor((x / 4096) + .5)
  152.     end
  153.     function getcamy()
  154.       local y1 = memory.readlong(0x02133AD0+8+ramshift4)*2 - memory.readlong(0x02133AD0+ramshift4)
  155.       local y2 = memory.readlong(0x02133B40+8+ramshift4)*2 - memory.readlong(0x02133B40+ramshift4)
  156.       return math.floor((math.min(y1,y2) / 4096) + .5)
  157.     end
  158.     function istopontop()
  159.       return memory.readlong(0x02133AD0+ramshift4) < memory.readlong(0x02133B40+ramshift4)
  160.     end
  161.     function isnotinlevel()
  162.       return memory.readword(0x213399C+ramshift) == 0
  163.     end
  164.     function getinstantid()
  165.       return memory.readword(0x021338C8+ramshift2)
  166.     end
  167.     function getperframeinfo()
  168.       --local baseaddr = memory.readlong(0x02133884+ramshift?)
  169.       --local rawx = memory.readlong(baseaddr + 0x44)
  170.       --local x = rawx/4096
  171.       --local y = memory.readlong(baseaddr + 0x48)/4096
  172.       local x = memory.readlong(0x0214389C+ramshift5)/4096
  173.       local y = memory.readlong(0x021438A0+ramshift5)/4096
  174.       return packxy(x,y)
  175.     end
  176.  
  177.   elseif whichgame == 'sr' then
  178.  
  179.     function getcamx()
  180.       local x = memory.readlong(0x0236AADC+8+ramshift)*2 - memory.readlong(0x0236AADC+ramshift)
  181.       return math.floor((x / 256) + .5)
  182.     end
  183.     function getcamy()
  184.       local y1 = memory.readlong(0x0236AAE0+8+ramshift)*2 - memory.readlong(0x0236AAE0+ramshift)
  185.       local y2 = memory.readlong(0x0236AB44+8+ramshift)*2 - memory.readlong(0x0236AB44+ramshift)
  186.       return math.floor((math.min(y1,y2) / 256) + .5)
  187.     end
  188.     function istopontop()
  189.       return memory.readlong(0x02090050+ramshift) < memory.readlong(0x02090058+ramshift)
  190.     end
  191.     function isnotinlevel()
  192.       return false -- nyi
  193.     end
  194.     function getinstantid()
  195.       return memory.readword(0x02091EBC+ramshift)
  196.     end
  197.     function getperframeinfo()
  198.       --local x = memory.readlong(0x020907DC+ramshift)/256
  199.       --local y = memory.readlong(0x020907E0+ramshift)/256
  200.       local x = memory.readlong(0x02090B54+ramshift)/256
  201.       local y = memory.readlong(0x02090B58+ramshift)/256
  202.       return packxy(x,y)
  203.     end
  204.  
  205.   elseif whichgame == 'nsmb' then
  206.  
  207.     function getcamx()
  208.       local x = memory.readlong(0x020CAF20) -- or 0x020CADCC (1 ahead)
  209.       return math.floor((x / 4096) + .5)
  210.     end
  211.     function getcamy()
  212.       return math.floor((memory.readlong(0x020CADDC) / -4096) + .5) - (istopontop() and 240 or 512) -- or 0x02085B78 (lower)
  213.     end
  214.     function istopontop()
  215.       return AND(memory.readbyte(0x020CAD0C),8) ~= 0
  216.     end
  217.     function isnotinlevel()
  218.       return memory.readwordsigned(0x021C5A62) == -1 -- or 0x021C5B5E or 0x021C5B82 -- not very good, only detects map screen
  219.     end
  220.     function getinstantid()
  221.       return memory.readword(0x020CAB14) -- or 0x2088BD8
  222.     end
  223.     function getperframeinfo()
  224.       local x = memory.readlong(0x0212AFC4)/4096 -- or 0x020CAEBC (1 ahead)
  225.       local y = memory.readlong(0x0212AFC8)/-4096 -- or 0x020CAEBC (1 ahead)
  226.       return packxy(x,y)
  227.     end
  228.  
  229.   end
  230.  
  231.   if not use_in_game_time then
  232.     getinstantid = function() return emu.framecount() end
  233.   end
  234.  
  235. end
  236.  
  237.  
  238. local speedtocolor, speedtocolornb
  239. if color_coded_speed then
  240.   speedtocolor = function (speed)
  241.     speed = math.abs(speed)
  242.     if speed < 512 then return 0x000000FF end
  243.     if speed < 2560 then return 0xFF0000FF end
  244.     if speed == 2560 then return 0xFF4000FF end
  245.     if speed < 3072 then return 0xFF7F00FF end
  246.     if speed == 3072 then return 0xFFAF00FF end
  247.     if speed < 3648 then return 0xFFFF00FF end
  248.     if speed <= 3840 then return 0xFFFFFFFF end
  249.     if speed < 4096 then return 0x7FFF7FFF end
  250.     if speed == 4096 then return 0x7FFFFFFF end
  251.     if speed < 8192 then return 0x00FFFFFF end
  252.     return 0xBF7FFFFF
  253.   end
  254.   speedtocolornb = function (speed)
  255.     local color = speedtocolor(speed)
  256.     if AND(color,0xFFFFFF00) == 0 then color = OR(color,0x7F7F7F00) end
  257.     return color
  258.   end
  259. else
  260.   speedtocolor = function (speed) return 0xFFFFFFFF end
  261.   speedtocolornb = function (speed) return 0xFFFFFFFF end
  262. end
  263.  
  264. function drawinfodisplay()
  265.   if input.get()[hide_key] then return end
  266.   if not draw_info_text then return end
  267.  
  268.   local topontop = istopontop()
  269.   drawinfo_y = not topontop and 20 or 20-192
  270.  
  271.   if isnotinlevel() then
  272.     drawinfo('...', '')
  273.     return
  274.   end
  275.  
  276.   local metery, timex, timey
  277.   if whichgame == 'sc' then
  278.     local baseaddr = memory.readlong(0x02120850+ramshift) -- (or 0x0212085C or 0x02120860)
  279.     xvel  = math.floor(memory.readlongsigned(baseaddr + 0xC0) / 16 + 0.5)
  280.     yvel  = math.floor(memory.readlongsigned(baseaddr + 0xC4) / 16 + 0.5)
  281.     speed = math.floor(memory.readlongsigned(baseaddr + 0xCC) / 16 + 0.5)
  282.     meter = (memory.readwordsigned(baseaddr + 0x654) * 300 / 4800)
  283.     power = (memory.readwordsigned(baseaddr + 0x820))
  284.     if memory.readbyte(0x021201EC+ramshift) == 2 then power = 0 end
  285.     time = memory.readword(0x0212089C+ramshift) -- level timer only
  286.     xpos = memory.readlong(baseaddr + 0x44) / 4096
  287.     ypos = memory.readlong(baseaddr + 0x48) / 4096
  288.     --local angval = memory.readshort(0x0212FF94+ramshift)
  289.     --if angval == 0 then angval = memory.readlong(baseaddr + 0x1AC) end
  290.     --angle = math.floor(-math.asin(angval / 4096) * (180 / math.pi) + 0.5)
  291.     --if angval > 4096 or angval < -4096 then angle = 0 end
  292.     metery = 160
  293.     timex = 160
  294.     timey = 11
  295.     if power > 0 then gui.text(0,not topontop and 48 or 48-192,power) end
  296.   elseif whichgame == 'sra' then
  297.     local baseaddr = memory.readlong(0x02133884+ramshift)
  298.     xvel  = math.floor(memory.readlongsigned(baseaddr + 0xBC) / 16 + 0.5)
  299.     yvel  = math.floor(memory.readlongsigned(baseaddr + 0xC0) / 16 + 0.5)
  300.     speed = math.floor(memory.readlongsigned(baseaddr + 0xC8) / 16 + 0.5)
  301.     meter = memory.readwordsigned(baseaddr + 0x5f8) * 300 / 4800
  302.     time = memory.readword(0x021338C8+ramshift2)
  303.     xpos = memory.readlong(baseaddr + 0x44) / 4096
  304.     ypos = memory.readlong(baseaddr + 0x48) / 4096
  305.     --angle = memory.readlongsigned(baseaddr + 0x34) * 180 / 32768 -- todo
  306.     --angle = angle<180 and -angle or 360-angle
  307.     --if -angle == 0 then angle = 0 end
  308.     metery = 46
  309.     timex = 152
  310.     timey = 17
  311.   elseif whichgame == 'sr' then
  312.     xvel  = memory.readwordsigned(0x020907E8+ramshift)
  313.     yvel  = memory.readwordsigned(0x020907EA+ramshift)
  314.     speed = memory.readwordsigned(0x020907F4+ramshift)
  315.     meter = memory.readwordsigned(0x02090B10+ramshift) * 300 / 4800
  316.     time = memory.readword(0x02091EBC+ramshift)
  317.     xpos = memory.readlong(0x020907DC+ramshift) / 256
  318.     ypos = memory.readlong(0x020907E0+ramshift) / 256
  319.     --local angval = memory.readshort(0x02090934+ramshift) -- todo
  320.     --angle = math.floor(-math.asin(angval / 4096) * (180 / math.pi) + 0.5)
  321.     --if angval > 4096 or angval < -4096 then angle = 0 end
  322.     metery = 41
  323.     timex = 82
  324.     timey = 17
  325.   elseif whichgame == 'nsmb' then
  326.     xvel  = math.floor(memory.readwordsigned(0x021B7104) / 4 + 0.5)
  327.     yvel  = math.floor(memory.readwordsigned(0x021B7108) / 4 + 0.5)
  328.     speed = math.floor(memory.readwordsigned(0x021B6A90) / 4 + 0.5)
  329.     meter = 0
  330.     time = memory.readword(0x020CAB14)
  331.     xpos = memory.readlong(0x020CAEBC) / 4096
  332.     ypos = memory.readlong(0x020CAEC0) / -4096
  333.     timex = 180
  334.     timey = 1
  335.   else
  336.     speed = 0
  337.     xvel = 0
  338.     meter = 0
  339.     metery = 0
  340.     timex = 0
  341.     timey = 0
  342.   end
  343.  
  344.   drawinfo('speed',speed,0,0,speedtocolornb(speed))
  345.   drawinfo('xvel',xvel,0,0,speedtocolornb(xvel))
  346.   drawinfo('yvel')
  347.   if meter > 0 then gui.text(0,not topontop and metery or metery-192,string.format('%.1f%%',meter)) end
  348.   gui.text(timex,not topontop and timey or timey-192,time,0x7FFFFFFF)
  349.   drawinfo('x', xpos, 0,-30)
  350.   drawinfo('y', ypos, 0,-30)
  351.   --drawinfo('angle', angle)
  352.  
  353. end
  354.  
  355.  
  356.  
  357.  
  358.  
  359.  
  360. -- draws the box of the given frame of a track
  361. function renderframeinfo(records, id,camx,camy, tracknumber)
  362.  
  363.   if not records then return end
  364.  
  365.   local packedinfo = records[id]
  366.   if not packedinfo then return end
  367.  
  368.   local xpos,ypos = unpackxy(packedinfo)
  369.  
  370.   local notcurrent = (tracknumber ~= currenttracknumber)
  371.  
  372.   -- don't draw anything if it's after the last frame
  373.   if notcurrent and (id > (records.endid or id)) then return end
  374.  
  375.   -- draw a box where the player is (in this track)
  376.   local hwidth = 10
  377.   local hheight = 16
  378.   local x = xpos - camx
  379.   local y = ypos - camy - 192
  380.   if y > 0 then
  381.     y = y - 80
  382.     if y <= 16 then return end
  383.   end
  384.  
  385.   -- force offscreen boxes to draw partially onscreen so we can know where they are
  386.   local offscreen
  387.   if x<0    then x=0    offscreen = true end
  388.   if x>256  then x=256  offscreen = true end
  389.   if y<-192 then y=-192 offscreen = true end
  390.   if y>192  then y=192  offscreen = true end
  391.  
  392.   local color        = records.important and 0xFF7F7F7F or 0x7F7FFF7F
  393.   local outlinecolor = notcurrent and 0x000000FF or speedtocolor(math.abs(speed or 0))
  394.   if not notcurrent then -- make filling more transparent if is current state
  395.     color = OR(AND(color,0xFFFFFF00),AND(color,0xFF)*0.5)
  396.   elseif offscreen then -- make more transparent if offscreen
  397.     color = OR(AND(color,0xFFFFFF00),AND(color,0xFF)*0.75)
  398.     outlinecolor = OR(AND(outlinecolor,0xFFFFFF00),AND(outlinecolor,0xFF)*0.75)
  399.   end
  400.   gui.drawbox(x-hwidth, y-hheight, x+hwidth, y+hheight, color, outlinecolor)
  401.  
  402. --  -- draw the track number somewhere in the box
  403. --  if notcurrent then
  404. --    x = x + (tracknumber%6) * 4 - 10
  405. --    y = y + (tracknumber/6) * 6 - 15
  406. --    local message = string.format("%d", tracknumber)
  407. --    gui.text(x, y, message, 0xFFFFFFFF)
  408. --  end
  409.  
  410.   -- draw the track's offset amount somewhere in the box
  411.   if notcurrent and records.offset and records.offset ~= 0 then
  412.     --x = x + (tracknumber%6) * 4 - 13
  413.     x = x - 8
  414.     y = y + (tracknumber/6) * 6 - 15
  415.     local message = string.format("%s%d", records.offset > 0 and '+' or '', records.offset)
  416.     gui.text(x, y, message, outlinecolor,color)
  417.   end
  418.  
  419.   if not offscreen then
  420.     records.seenago = 0
  421.   end
  422.  
  423. end
  424.  
  425.  
  426. function ispointwithintrackbox(records, id,camx,camy, tracknumber, xcheck,ycheck)
  427.  
  428.   if not records then return end
  429.   local packedinfo = records[id]
  430.   if not packedinfo then return end
  431.  
  432.   local xpos,ypos = unpackxy(packedinfo)
  433.  
  434.   local notcurrent = (tracknumber ~= currenttracknumber)
  435.  
  436.   -- don't check if it's after the last frame
  437.   if notcurrent and (id > (records.endid or id)) then return end
  438.  
  439.   -- check a box where the player is (in this track)
  440.   local hwidth = 10
  441.   local x = xpos - camx
  442.  
  443.   -- offscreen boxes draw partially onscreen so you can interact with them
  444.   if x<0    then x=0   end
  445.   if x>256  then x=256 end
  446.  
  447.   if xcheck < x-hwidth or xcheck > x+hwidth then return end
  448.  
  449.   local hheight = 16
  450.   local y = ypos - camy - 192
  451.   if y > 0 then
  452.     y = y - 80
  453.     if y <= 16 then return end
  454.   end
  455.  
  456.   -- offscreen boxes draw partially onscreen so you can interact with them
  457.   if y<-192 then y=-192 end
  458.   if y>192  then y=192  end
  459.  
  460.   if ycheck < y-hheight or ycheck > y+hheight then return end
  461.  
  462.   return true
  463.  
  464. end
  465.  
  466.  
  467. local points = {}
  468. local ids = {}
  469. function renderpasttrajectory(records, id, tracknumber)
  470.  
  471.   if not records or not draw_velocity_trails then return end
  472.  
  473.   local lastx
  474.   local j = 1
  475.   for i=id,id-64,-1 do
  476.     local info = records[i]
  477.     if info then
  478.       points[j] = info
  479.       ids[j] = i
  480.       j = j + 1
  481.     end
  482.   end
  483.   for k=j,#points do
  484.     points[k] = nil
  485.     ids[k] = nil
  486.   end
  487.  
  488.   local camx = getcamx()
  489.   local camy = getcamy() + 192
  490.  
  491.   if #points < 2 then return end
  492.  
  493.   local ax,ay = unpackxy(points[1])
  494.   local bx,by = unpackxy(points[2])
  495.   ax = ax - camx
  496.   ay = ay - camy
  497.   bx = bx - camx
  498.   by = by - camy
  499.  
  500.   local speedfactor = (whichgame == 'nsmb' and 4 or 1)
  501.  
  502.   local skipfirst = false
  503.   for i=3,j-1 do
  504.    
  505.     local aym = ay-16
  506.     local ayp = ay+16
  507.     local bym = by-16
  508.     local byp = by+16
  509.    
  510.     if aym > 0 then aym = math.max(0,aym - 80) end
  511.     if ayp > 0 then ayp = math.max(0,ayp - 80) end
  512.     if bym > 0 then bym = math.max(0,bym - 80) end
  513.     if byp > 0 then byp = math.max(0,byp - 80) end
  514.    
  515.     local transp = 255 * (j+3-i) / j
  516.  
  517.     local dx = bx - ax
  518.     local dy = by - ay
  519.     local di = ids[i-2] - ids[i-1]
  520.     local xspeed = math.abs(dx) * speedfactor / di
  521.    
  522.     if math.abs(dx) < 256 and math.abs(dy) < 192 then
  523.       local color = OR(AND(speedtocolor(xspeed*256),0xFFFFFF00), transp)
  524.       --local color = OR(AND(speedtocolor((i%2)*2560),0xFFFFFF00), transp)
  525.    
  526.       if (dx < 0 or dy < 0) and (aym ~= 0 or bym ~= 0) then
  527.         gui.drawline(ax-10,aym,bx-10,bym, color, skipfirst)
  528.       end
  529.       if (dx < 0 or dy > 0) and (ayp ~= 0 or byp ~= 0) then
  530.         gui.drawline(ax-10,ayp,bx-10,byp, color, skipfirst)
  531.       end
  532.       if (dx > 0 or dy < 0) and (aym ~= 0 or bym ~= 0) then
  533.         gui.drawline(ax+10,aym,bx+10,bym, color, skipfirst)
  534.       end
  535.       if (dx > 0 or dy > 0) and (ayp ~= 0 or byp ~= 0) then
  536.         gui.drawline(ax+10,ayp,bx+10,byp, color, skipfirst)
  537.       end
  538.  
  539.       skipfirst = true
  540.     end  
  541.  
  542.     ax=bx
  543.     ay=by
  544.     bx,by = unpackxy(points[i])
  545.     bx = bx - camx
  546.     by = by - camy
  547.  
  548.   end
  549.  
  550. end
  551.  
  552.  
  553.  
  554.  
  555. function drawghostdisplay()
  556.  
  557.   local inp = input.get()
  558.  
  559.   if inp[hide_key] then return end
  560.  
  561.   if isnotinlevel() then return end
  562.  
  563.   -- calculate/retrieve the frame info
  564.   local id = getinstantid()
  565.   if id < 2 then return end
  566.   local camx = getcamx()
  567.   local camy = getcamy()
  568.  
  569.   if draw_ghosts then
  570.  
  571.     -- draw hitboxes for this frame for all tracks except the current one
  572.     for tracknumber,records in ipairs(frameinforecords) do
  573.       if tracknumber ~= currenttracknumber then
  574.         local tempid = id
  575.         if records and records.offset then
  576.           tempid = tempid + records.offset
  577.         end
  578.         renderframeinfo(records, tempid,camx,camy, tracknumber)
  579.       end
  580.     end
  581.  
  582.   end
  583.  
  584.   -- draw velocity trails for the current track
  585.   renderpasttrajectory(frameinforecords[currenttracknumber], id, currenttracknumber)
  586.  
  587.   -- draw the information for this frame for the current track
  588.   renderframeinfo(frameinforecords[currenttracknumber], id,camx,camy, currenttracknumber)
  589.  
  590. end
  591.  
  592.  
  593. function figureoutwhichgame()
  594.   if emu.framecount() < 10 then return end
  595.   local gameid = memory.readlongunsigned(0x200000C)
  596.   if gameid == 0xF509DEFF or gameid == 0x72E9DEFF or gameid == 0x023FDEFF then
  597.     whichgame = 'sc'
  598.     ramshift = (gameid == 0x023FDEFF) and -0x20 or 0
  599.     print('Sonic Colo'..(gameid == 0x72E9DEFF and 'u' or '')..'rs detected')
  600.   elseif gameid == 0xE481DEFF or gameid == 0x33A9DEFF or gameid == 0xB936DEFF or gameid == 0xAD6FDEFF then
  601.     whichgame = 'sra'
  602.     ramshift = (gameid == 0xAD6FDEFF) and -0x800 or 0
  603.     ramshift2 = (gameid == 0xAD6FDEFF) and -0x7F0 or 0
  604.     ramshift3 = (gameid == 0xAD6FDEFF) and -0x794 or 0
  605.     ramshift4 = (gameid == 0xAD6FDEFF) and -0x804 or 0
  606.     ramshift5 = (gameid == 0xAD6FDEFF) and 0x3CC or 0
  607.     print('Sonic Rush Adventure detected')
  608.   elseif gameid == 0x9771DEFF or gameid == 0x97EDDEFF or gameid == 0x187BDEFF then
  609.     whichgame = 'sr'
  610.     ramshift = (gameid == 0x187BDEFF) and 0x20 or 0
  611.     print('Sonic Rush detected')
  612.   elseif gameid == 0xA315DEFF then
  613.     whichgame = 'nsmb'
  614.     print('New Super Mario Bros. detected')
  615.   elseif not checkedwhichgame then
  616.     print(string.format('unknown game %X',gameid))
  617.   end
  618.   if whichgame then initgamespecificfunctions() end
  619.   checkedwhichgame = true
  620. end
  621.  
  622. emu.registerafter( function()
  623.  
  624.   if not whichgame then figureoutwhichgame() end
  625.  
  626.   if isnotinlevel() then return end
  627.  
  628.   id = getinstantid()
  629.  
  630.   if id == 0 then return end
  631.  
  632.   -- just in case...
  633.   if not frameinforecords[currenttracknumber] then frameinforecords[currenttracknumber] = {} end
  634.  
  635.   -- store the new information for this frame
  636.   prevperframeinfo = getperframeinfo()
  637.   local currecords = frameinforecords[currenttracknumber]
  638.   currecords[id] = prevperframeinfo
  639.   currecords.endid = id
  640.   currecords.endinfo = prevperframeinfo
  641.   currecords.seenago = 0
  642.  
  643.   -- update "last seen" counter for each track
  644.   for tracknumber,records in ipairs(frameinforecords) do
  645.     records.seenago = (records.seenago or 0) + 1
  646.   end
  647.  
  648. end)
  649.  
  650.  
  651.  
  652.  
  653. emu_registeridle = (function()
  654.  
  655.   if not whichgame and not checkedwhichgame then figureoutwhichgame() end
  656.  
  657.   -- hack to detect when a savestate got loaded, because desmume currently lacks savestate.registerload
  658.   local curid = getinstantid()
  659.   local curframeinfo = getperframeinfo()
  660.   local removenum
  661.   if id ~= curid or prevperframeinfo ~= curframeinfo then
  662.  
  663.     local prevrecords = frameinforecords[currenttracknumber]
  664.     local previnfo = prevperframeinfo
  665.     local previd = id
  666.  
  667.     id = curid
  668.     prevperframeinfo = curframeinfo
  669.    
  670.     -- savestate got loaded...
  671.    
  672.     -- sort tracks, with higher indices being less important to keep
  673.     table.sort(frameinforecords, function(a,b)
  674.       if not a then return b ~= nil end -- probably not needed, shouldn't be
  675.       if (a==prevrecords) ~= (b==prevrecords) then return a==prevrecords end -- don't discard the track we just recorded
  676.       if a.important ~= b.important then return (a.important and 1 or 0) > (b.important and 1 or 0) end -- avoid discarding protected/important tracks
  677.       if a.seenago ~= b.seenago then return (a.seenago or 9999) < (b.seenago or 9999) end -- discard "least recently seen"
  678.       local ax,_ = unpackxy(a.endinfo or 0)
  679.       local bx,_ = unpackxy(b.endinfo or 0)
  680.       return ax*(b.endid or 0) > bx*(a.endid or 0) -- use average x velocity as tiebreaker
  681.     end)
  682.    
  683.     -- figure out where the previous value of currenttracknumber got moved to by the sorting
  684.     local prevtracknumber
  685.     for tracknumber,records in ipairs(frameinforecords) do
  686.       if records == prevrecords then
  687.         prevtracknumber = tracknumber
  688.         break
  689.       end
  690.     end
  691.    
  692.     -- use next unoccupied track, or replace worst track if out of space
  693.     currenttracknumber = 1+#frameinforecords
  694.     if currenttracknumber > max_ghosts then currenttracknumber = max_ghosts end
  695.    
  696.     -- if the new state matches the middle of another track, copy that track to the new one being selected
  697.     for tracknumber,records in ipairs(frameinforecords) do
  698.       if records[curid] == curframeinfo and tracknumber ~= currenttracknumber then
  699.         frameinforecords[currenttracknumber] = copytable(records)
  700.         frameinforecords[currenttracknumber].endid = curid
  701.         frameinforecords[currenttracknumber].endinfo = curframeinfo
  702.         break
  703.       end
  704.     end
  705.    
  706.     -- if the previous state matches the middle of another track, delete the previous state's track
  707.     if frameinforecords[prevtracknumber] then
  708.       for tracknumber,records in ipairs(frameinforecords) do
  709.         if records[previd] == previnfo and tracknumber ~= prevtracknumber and tracknumber ~= currenttracknumber then
  710.           removenum = prevtracknumber
  711.           break
  712.         end
  713.       end
  714.     end
  715.   end
  716.  
  717.  
  718.   -- if the current state matches the end of another track, delete that track
  719.   if not removenum and frameinforecords[currenttracknumber] and frameinforecords[currenttracknumber].endid == curid and frameinforecords[currenttracknumber].endinfo == curframeinfo then
  720.     for tracknumber,records in ipairs(frameinforecords) do
  721.       if records.endid == curid and records.endinfo == curframeinfo and tracknumber ~= currenttracknumber then
  722.         --removenum = tracknumber
  723.         local important = frameinforecords[tracknumber].important
  724.         frameinforecords[tracknumber] = frameinforecords[currenttracknumber]
  725.         frameinforecords[tracknumber].important = important or frameinforecords[tracknumber].important
  726.         removenum = currenttracknumber
  727.         currenttracknumber = tracknumber
  728.         break
  729.       end
  730.     end
  731.   end
  732.  
  733.   -- actually remove whatever we decided to, if anything
  734.   if removenum then
  735.     table.remove(frameinforecords, removenum)
  736.     if currenttracknumber > removenum then
  737.       currenttracknumber = currenttracknumber - 1
  738.     end
  739.   end
  740.  
  741.  
  742.   -- check for removing a track via right-clicking on it
  743.   local inp = input.get()
  744.   if inp[delete_ghost_key] and not removecheckwasrightclick then
  745.     --while true do
  746.       removenum = nil
  747.      
  748.       local id = getinstantid()
  749.       local camx = getcamx()
  750.       local camy = getcamy()
  751.  
  752.       for tracknumber,records in ipairs(frameinforecords) do
  753.         local tempid = id
  754.         if frameinforecords[tracknumber] and frameinforecords[tracknumber].offset and tracknumber ~= currenttracknumber then
  755.           tempid = tempid + frameinforecords[tracknumber].offset
  756.         end
  757.         if ispointwithintrackbox(records, tempid,camx,camy, tracknumber, inp.xmouse, inp.ymouse) then
  758.           removenum = tracknumber
  759.           break
  760.         end
  761.       end
  762.  
  763.       if removenum then
  764.         if frameinforecords[removenum].important then
  765.           frameinforecords[removenum].important = nil -- strip importance before deleting
  766.         elseif removenum == currenttracknumber then
  767.           frameinforecords[removenum] = {}
  768.           frameinforecords[removenum].seenago = 0
  769.         else
  770.           table.remove(frameinforecords, removenum)
  771.           if currenttracknumber > removenum then
  772.             currenttracknumber = currenttracknumber - 1
  773.           end
  774.         end
  775.         removecheckwasrightclick = inp[delete_ghost_key] -- only remove 1 per click-drag
  776.       end
  777.      
  778.       --if not removenum then break end -- only remove 1 per click
  779.     --end
  780.   end
  781.   if not inp[delete_ghost_key] then removecheckwasrightclick = rightclick end
  782.  
  783.  
  784.   -- check for marking a track as important via left-clicking on it
  785.   if inp[protect_ghost_key] and not markcheckwasleftclick then
  786.     --while true do
  787.       local id = getinstantid()
  788.       local camx = getcamx()
  789.       local camy = getcamy()
  790.  
  791.       for tracknumber,records in ipairs(frameinforecords) do
  792.         local tempid = id
  793.         if frameinforecords[tracknumber] and frameinforecords[tracknumber].offset and tracknumber ~= currenttracknumber then
  794.           tempid = tempid + frameinforecords[tracknumber].offset
  795.         end
  796.         if ispointwithintrackbox(records, tempid,camx,camy, tracknumber, inp.xmouse, inp.ymouse) and not frameinforecords[tracknumber].important then
  797.           frameinforecords[tracknumber].important = true
  798.           markcheckwasleftclick = inp[protect_ghost_key] -- only mark 1 per click-drag
  799.           break
  800.         end
  801.       end
  802.      
  803.       --if not removenum then break end -- only mark 1 per click
  804.     --end
  805.   end
  806.   if not inp[protect_ghost_key] then markcheckwasleftclick = leftclick end
  807.  
  808.  
  809.  
  810.   -- check for adjusting a track's offset via [ and ] while moused over it
  811.   if inp[adjust_ghost_backward_key] or inp[adjust_ghost_forward_key] then
  812.     local id = getinstantid()
  813.     local camx = getcamx()
  814.     local camy = getcamy()
  815.     for tracknumber,records in ipairs(frameinforecords) do
  816.       if tracknumber ~= currenttracknumber then
  817.         local tempid = id
  818.         if frameinforecords[tracknumber] and frameinforecords[tracknumber].offset then
  819.           tempid = tempid + frameinforecords[tracknumber].offset
  820.         end
  821.         if ispointwithintrackbox(records, tempid,camx,camy, tracknumber, inp.xmouse, inp.ymouse) then
  822.           local newoffset = (frameinforecords[tracknumber].offset or 0) + (inp[adjust_ghost_backward_key] and -1 or 1)
  823.           if inp[adjust_ghost_backward_key] and inp[adjust_ghost_forward_key] then newoffset = 0 end
  824.           local newtempid = id + newoffset
  825.           if newtempid == 0 or ((newtempid <= (records.endid or newtempid)) and frameinforecords[tracknumber][newtempid]) then -- don't let it disappear
  826.             if newoffset == 0 then newoffset = nil end
  827.             frameinforecords[tracknumber].offset = newoffset
  828.           end
  829.         end
  830.       end
  831.     end
  832.   end
  833.  
  834. end)
  835.  
  836.  
  837.  
  838. gui.register(function()
  839.   emu_registeridle() -- update some things even while paused, calling this here is a hack and might rely on desmume's dual-core mode
  840.   drawghostdisplay()
  841.   drawinfodisplay()
  842. end)
RAW Paste Data