Post subject: Rewind Lua script (and other fun scripts)
Joined: 3/15/2009
Posts: 8
Hello everyone. Having recently discovered the new release of Gens Rerecording (11a) with Lua support and all, I decided to have some fun with it. I noticed that Rerecording has pause and slowdown, but no rewind. So I took a look at the various Lua samples and documentation, and noticed that I could implement it myself. After I was successful, I decided to post it here.
-- Rewind script
-- Rewinds emulation in realtime (or faster)
-- Written by: deltaphc

-- Constants
max_states = 300    -- maximum number of states allowed
rewind_interval = 3 -- save state every N frames

states = {}
current_state = 1
rewind_counter = 1
rewinding = false

input.registerhotkey(1, function()
	rewinding = not rewinding
	
	if rewinding then
		gens.message("Rewind: On")
	else
		gens.message("Rewind: Off")
	end
end)

gens.registerafter( function()
	if rewinding then
		current_state = current_state - 1
		if current_state < 1 then
			current_state = 1
			rewinding = false
			gens.message("Rewind: Off")
		end

		savestate.load(states[current_state])
		table.remove(states, current_state + 1)
	else
		if rewind_counter == rewind_interval then
			rewind_counter = 1

			if current_state < max_states then
				local state = savestate.create()
				savestate.save(state)
				table.insert(states, state)

				current_state = current_state + 1
			else
				table.remove(states, 1)

				local state = savestate.create()
				savestate.save(state)
				table.insert(states, state)
			end
		end
		
		rewind_counter = rewind_counter + 1
	end
end)
Start a game, run script, play for awhile, then press Lua Hotkey 1. Everything goes backwards! :) By default it saves states for up to 10 seconds, but the limit is adjustable via the max_states and rewind_interval variables at the top. A warning: This script tends to use up a lot of memory. Set the limits too high, and you could run into OS problems depending upon how much RAM you have. Enjoy. ;) Edit 1: Revised script (thanks Xk). Rewind is now a toggle (Lua Hotkey 1), and save interval is adjustable (defaults to 6). Edit 2: Fixed freeze when rewind reached the beginning; it automatically leaves rewind mode. Also, changed defaults to something slightly more sensible. A 6 frame interval seemed a bit too fast. Old versions: Revision 1, Revision 2
Banned User
Joined: 12/23/2004
Posts: 1850
Set a "rewind interval". Very rarely is it useful to go back exactly one frame. If you cut it so it only saves once every 6 frames, you can have 10 seconds and save 5/6th memory. Similarly, if you save every second, you can raise that backlog to several minutes. Another idea that preserves "short term" rewind is to have three stages: - Most recent 12 frames - An interval of every 12th frame after that, up to 9 - An interval of every second after that This would allow a condensed rewind range of ~2 seconds. There are a lot of ways to conserve memory, though, and the "only every x frames" method would be a lot easier to implement.
Perma-banned
Joined: 3/15/2009
Posts: 8
Updated OP with revised script. Though I suppose the three-stage approach is better in the long-run; it would conserve memory while allowing much longer rewinds. I'll probably implement it when I become un-lazy. :P
honorableJay
He/Him
Joined: 8/18/2008
Posts: 104
Location: Albany, NY
It's been a while since I've done any programming, but I do have a question with regards to how the script runs. Basically you have save slots 1-6, and if you're rewinding gens will count backwards until it reaches the save slot in 1. But when you save the states, it looks like you'll keep looping in a circle, meaning once you get to state 6 you'll jump back to 1 and start counting up again. How does this affect rewinding if you've been playing for a while and you start rewinding from save state 3? Will it rewind back to save state 1 then stop? Just to make sure I'm interpreting this correctly, this is how I'm visualizing the script working. Save states 1-6 are all in a line: 1 2 3 4 5 6 Once the game is loaded, the script begins saving in order: 1 2 3 4 5 6 Once it reaches 6, it loops back to 1 and begins again in order: 1 2 3 4 5 6 Now say I play a game for a minute and I happen to be on save state 3, the line is now in this order: 4 5 6 1 2 3 If I try to rewind to the earliest save state, which would be 4, the script will stop at 1 (if current_state < 1 then current_state = 1 end) meaning I've only been able to back up in this order: 3 2 1 Slots 4 5 and 6 aren't checked to see if they're older than state 1, giving me no access to them during rewind. Technically wouldn't it have to rewind to state 1, then jump to 6 and count down to 4? Maybe my logic is off, but with the saving constantly looping around, but the loading not looping wouldn't you miss out on a few of the save states if you don't start rewinding from the 6th save state? If this is the case, the only fix I can think of would be to have the loading also loop, but to check and see if the save slot you're trying to load is the same one you started on, in which case it will not load the state and stop rewinding.
Emulator Coder, Skilled player (1299)
Joined: 12/21/2004
Posts: 2687
It looks like it keeps up to 100 savestates at a time, and they don't loop around (it simply discards the oldest ones, although I'd suggest changing it to recycle their memory). Also, they're not stored in the same save slots like 1-9 that the user can load, they're just savestate objects. The way it's written now, I think rewinding will simply undo savestate loads if you rewind through them. By the way, I wish I hadn't made input.registerhotkey work like that. Using an existing "hotkey slot" was pure laziness on my part and creates all sorts of problems. It should add a new configurable hotkey entry with a default key press instead. Maybe in a future version... EDIT: I assume the post after this is also referring to honorableJay's post instead of this one.
Joined: 3/15/2009
Posts: 8
Edit: Yes, it was indeed referring to the post above yours. :) 1. When it's recording (i.e. not rewinding), it continually saves states into the 'states' table until it reaches max_states. 2. When it reaches max_states, what it does is remove element #1 (first element) from the states table using table.remove(). As a side-effect this also pushes all of the existing elements back, freeing up the very last slot in the states table. It then inserts a new state into the last slot. 3. When it rewinds, it decrements current_state, loads the state at that position, then removes the slot that was previously loaded. 4. When it resumes recording, it just continues where it left off at current_state, incrementing until it reaches max_states. Repeat from step 2.
Joined: 3/15/2009
Posts: 8
Double post. A useful double post, though. ;) Updated OP with third revision. Just a couple minor changes. Also, been messing around with other things. Here's a script that lets you change the zone/act in Sonic 1/2/3/K via clickable buttons:
addr_zone = 0xfffe10
addr_act = 0xfffe11
addr_restartflag = 0xfffe02

input_state = {}

gui.register( function()
	input_state = input.get()
	
	local zone = memory.readbyte(addr_zone)
	local act = memory.readbyte(addr_act)
	local restartflag = memory.readbyte(addr_restartflag)
	
	gui.text(0, 217, string.format("Zone %X", zone))
	if restartflag == 0 then
		if do_button("-", 32, 216, 6, 7) then
			memory.writebyte(addr_zone, zone - 1)
			memory.writebyte(addr_restartflag, 1)
		end
		if do_button("+", 39, 216, 6, 7) then
			memory.writebyte(addr_zone, zone + 1)
			memory.writebyte(addr_restartflag, 1)
		end
	end
	
	gui.text(52, 217, string.format("Act %X", act))
	if restartflag == 0 then
		if do_button("-", 80, 216, 6, 7) then
			memory.writebyte(addr_act, act - 1)
			memory.writebyte(addr_restartflag, 1)
		end
		if do_button("+", 87, 216, 6, 7) then
			memory.writebyte(addr_act, act + 1)
			memory.writebyte(addr_restartflag, 1)
		end
	end
	
	if restartflag == 0 then
		if do_button("Restart", 100, 216, 30, 7) then
			memory.writebyte(addr_restartflag, 1)
		end
	end
end)

function do_button(text, x, y, width, height)
	local return_value, fill_color, outline_color
	
	if (input_state.xmouse >= x and input_state.ymouse >= y) and
		(input_state.xmouse <= x + width and input_state.ymouse <= y + height) then
		outline_color = {255, 255, 255, 255}
		
		if input_state.leftclick then
			return_value = true
			fill_color = {127, 0, 0, 255}
		else
			fill_color = {0, 0, 192, 255}
		end
	else
		fill_color = {0, 0, 127, 255}
		outline_color = {0, 0, 255, 255}
	end

	gui.box(x, y, x + width, y + height, fill_color, outline_color)
	gui.text(x + 2, y + 2, text, {255, 255, 255, 255}, {0, 0, 0, 0})
	
	return return_value
end
Screenshot:
Emulator Coder, Skilled player (1299)
Joined: 12/21/2004
Posts: 2687
This isn't so useful, but here's a little script that applies extreme motion blur to everything:
gens.registerbefore(function() gui.image(gui.gdscreenshot(), 0.8) end)
Banned User
Joined: 12/23/2004
Posts: 1850
nitsuja wrote:
This isn't so useful, but here's a little script that applies extreme motion blur to everything:
gens.registerbefore(function() gui.image(gui.gdscreenshot(), 0.8) end)
duuuuuuuuuuuuuude. This is the kind of shit that makes me wish FCEUX actually had a 32-bit canvas instead of that 8-bit shit it has now.
Perma-banned
Skilled player (1633)
Joined: 11/15/2004
Posts: 2202
Location: Killjoy
nitsuja wrote:
This isn't so useful, but here's a little script that applies extreme motion blur to everything:
gens.registerbefore(function() gui.image(gui.gdscreenshot(), 0.8) end)
This made sonic fun to watch again! It'd be cool if the script only functioned when sonic's x velocity reached a certain point, and then varied between .8 to .95. (.95 is trippy!) Its sort of silly to have motion blur when sonic is barely moving.
Sage advice from a friend of Jim: So put your tinfoil hat back in the closet, open your eyes to the truth, and realize that the government is in fact causing austismal cancer with it's 9/11 fluoride vaccinations of your water supply.
Editor, Expert player (2457)
Joined: 4/8/2005
Posts: 1573
Location: Gone for a year, just for varietyyyyyyyyy!!
How about a random input generator?
Skilled player (1633)
Joined: 11/15/2004
Posts: 2202
Location: Killjoy
Aqfaq wrote:
How about a random input generator?
That is easy.
gens.registerbefore(function()

ctemp = math.random(1,255);
  if math.fmod(ctemp,2)==1 then
		joypad.set(1,{up=true});
	end;
	ctemp = math.floor(ctemp/2);
   if math.fmod(ctemp,2)==1 then
		joypad.set(1,{down=true});
	end;
	ctemp = math.floor(ctemp/2);
	if math.fmod(ctemp,2)==1 then
		joypad.set(1,{left=true});
	end;
	ctemp = math.floor(ctemp/2);
	if math.fmod(ctemp,2)==1 then
		joypad.set(1,{right=true});
	end;
	ctemp = math.floor(ctemp/2);
	if math.fmod(ctemp,2)==1 then
		joypad.set(1,{A=true});
	end;
	ctemp = math.floor(ctemp/2);
	if math.fmod(ctemp,2)==1 then
		joypad.set(1,{B=true});
	end;
	ctemp = math.floor(ctemp/2);
	if math.fmod(ctemp,2)==1 then
		joypad.set(1,{C=true});
	end;
	ctemp = math.floor(ctemp/2);
	if math.fmod(ctemp,2)==1 then
		joypad.set(1,{start=true});
	end;
end);															






Sage advice from a friend of Jim: So put your tinfoil hat back in the closet, open your eyes to the truth, and realize that the government is in fact causing austismal cancer with it's 9/11 fluoride vaccinations of your water supply.
Player (66)
Joined: 5/5/2007
Posts: 65
DarkKobold wrote:
It'd be cool if the script only functioned when sonic's x velocity reached a certain point
Simple.

	romcheck1 = memory.readbyte(0x00013f)
	if romcheck1 == 71 then addr_xspeed = 0xffd010 end
	-- Sonic 1

	if romcheck1 == 50 then addr_xspeed = 0xffb010 end
	-- Sonic 2

	if romcheck1 == 32 then addr_xspeed = 0xffb018 end
	-- Sonic 3&K

gens.registerbefore(function()
        xspeed = memory.readwordsigned(addr_xspeed)
	if (xspeed >= 1500 and xspeed <= 2500) or (xspeed <= -1500 and xspeed >= -2500) then gui.image(gui.gdscreenshot(), 0.8) end
	if (xspeed >= 2500) or (xspeed <= -2500) then gui.image(gui.gdscreenshot(), 0.92) end

	message = string.format("velocity: %d", xspeed)
	gui.text(10, 50, message, "#FFFF00FF", "black")


end)
romcheck1 checks a certain spot in the header to determine what game it is and where the x-velocity is stored. If your copy doesn't get the blur, use this to find the byte and add it to the romcheck list:
gens.registerbefore(function()

	thebyte = memory.readbyte(0x00013f)

	message = string.format("byte: %d", thebyte)
	gui.text(10, 50, message, "#FFFF00FF", "black")

end)
EDIT: Fixed as per nitsuja's suggestion below.
Emulator Coder, Skilled player (1299)
Joined: 12/21/2004
Posts: 2687
RattleMan wrote:
EDIT: The forum isn't displaying a line of the code correctly
I think you have to make sure the box that says "Disable HTML in this post" is checked before posting something like that. (You can set it permanently in your profile.) EDIT: here was my version of basically the same thing (S3K only).
gens.registerbefore(function()
	local xvel = memory.readwordsigned(0xFFB018)
	local yvel = memory.readwordsigned(0xFFB01A)
	local sonicspeed = math.sqrt((xvel*xvel)+(yvel*yvel))
	local bluramt = math.min(0.92, sonicspeed / 3072)
	gui.drawimage(gui.gdscreenshot(), bluramt)
end)
I'm not sure it's really better to make it depend on Sonic's speed though. Maybe what it should be checking is how fast the camera is moving through the world.
Skilled player (1633)
Joined: 11/15/2004
Posts: 2202
Location: Killjoy
Kick ass RattleMan! Here is a slight change to make the effect graded.
   romcheck1 = memory.readbyte(0x00013f)
   if romcheck1 == 71 then addr_xspeed = 0xffd010 end
   -- Sonic 1

   if romcheck1 == 50 then addr_xspeed = 0xffb010 end
   -- Sonic 2

   if romcheck1 == 32 then addr_xspeed = 0xffb018 end
   -- Sonic 3&K

gens.registerbefore(function()
        xspeed = memory.readwordsigned(addr_xspeed)
        xspeed = math.abs(xspeed);
        if (xspeed > 1500) then
        	xspeedt = math.min(xspeed, 3500); 
        	sup = math.floor((3500-xspeedt)/100)/100;
        	gui.image(gui.gdscreenshot(), (0.75+sup))    
         end;
   message = string.format("velocity: %d", xspeed)
   gui.text(10, 50, message, "#FFFF00FF", "black")
end)
Sage advice from a friend of Jim: So put your tinfoil hat back in the closet, open your eyes to the truth, and realize that the government is in fact causing austismal cancer with it's 9/11 fluoride vaccinations of your water supply.
Emulator Coder, Skilled player (1299)
Joined: 12/21/2004
Posts: 2687
DarkKobold wrote:
Aqfaq wrote:
How about a random input generator?
That is easy. ...
Here's a condensed version of yours:
local buttonmap = {[1]='up',[2]='down',[4]='left',[8]='right',[16]='A',[32]='B',[64]='C',[128]='start'}
gens.registerbefore(function()
  local temp = math.random(0,255)
  for bit,button in pairs(buttonmap) do
    if AND(temp,bit) ~= 0 then
      joypad.set({[button]=true})
    end
  end
end)
And here's one that makes it easier to adjust the chances per button:
local function setbuttonrandomly (button, chance)
	joypad.set({[button]=math.random()<chance})
end

gens.registerbefore(function()
	setbuttonrandomly("right", 0.7)
	setbuttonrandomly("left", 0.1)
	setbuttonrandomly("down", 0.15)
	setbuttonrandomly("A", 0.2)
	setbuttonrandomly("B", 0.1)
end)
Finally, here's a simple macro-playing script (somewhat stripped-down but probably already close to being usable):
-- define a macro to jump right from a standstill faster than normal
-- (at least that's what it does in Popful Mail)
jumprightmacro = {
	{B=false,right=true}, -- frame 1, hold right and don't jump yet
	{B=false,right=true}, -- frame 2, hold right and don't jump yet
	{B=true},             -- frame 3, actually jump
	{right=false}, -- frames 4-8, don't press right
	{right=false}, -- (anything else is ok, but right would
	{right=false}, --  slow you down if pressed anytime
	{right=false}, --  during these frames)
	{right=false},
}

-- more macro definitions like that would go here...
--spindashmacro = { {down=true,left=false,right=false}, {down=true,A=true}, {down=true,B=true},  {down=true,C=true}, {down=true,A=true}, {down=true,B=true}, {down=true,C=true}, {down=false} }


-- this function advances 1 frame (fast) during a macro
function doframewith (buttons)
	repeat
		joypad.set(buttons)
		gens.emulateframe() gens.wait()
	until abortmacro or not gens.lagged()
end

-- this is the function to start playing a macro
function domacro (buttonseq)
	abortmacro = false
	for i=1,#buttonseq do
		if abortmacro then return end
		doframewith(buttonseq[i])
	end
end

-- cancel the macro if I load a savestate while it's playing
savestate.registerload( function(statenumber)
	if type(statenumber) == 'number' then
		abortmacro = true
	end
end)


-- play the jumprightmacro whenever I hold Ctrl and Right at the same time
-- (could use registerhotkey instead, or a clickable button, or whatever)
gens.registerbefore(function()
	if alreadyin then return else alreadyin = true end

	if input.get().control and joypad.peekdown().right then
		domacro(jumprightmacro)
	end	
	
	-- more checks to do other macros would go here...

	alreadyin = false
end)
It starts getting more complicated fast if you want it to support things like layering macros on top of each other, continuing the rest of a macro when you load a savestate that was saved before it finished playing, making the macro playing context-sensitive to what would be useful to do in the game at the time, or allowing recording macros without changing the script, but those should all be possible to add on.
Joined: 3/15/2009
Posts: 8
Cool stuff, guys. For the past day or so, I've been working on something similar to Xk's SMB1 mouse control script. Due to the length of the script, I put it on pastebin: http://pastebin.ca/1364116 Objects mode: Drag stuff around, right-click to pin objects. Tiles mode: Pretty self-explanatory. A minor problem though; dragging an object too quickly will likely "let go" of it, because the mouse position is outside of the box. I know there's a solution to this, but it's escaping me at the moment. Edit: Fixed. Thanks upthorn.
upthorn
He/Him
Active player, Emulator Coder (387)
Joined: 3/24/2006
Posts: 1802
deltaphc wrote:
A minor problem though; dragging an object too quickly will likely "let go" of it, because the mouse position is outside of the box. I know there's a solution to this, but it's escaping me at the moment.
When clicked, save the ID of the object that is clicked (ID in this case being base RAM address, or index in the Sprite Status Table). Until you check input and find that the mouse button is not held any longer, drag that object to match the cursor.
How fleeting are all human passions compared with the massive continuity of ducks.
Joined: 3/15/2009
Posts: 8
Updated above post with fix. Also, added tiles mode while I was at it. For now, tile editing is disabled on S3/K due to its slightly more complicated way of getting row length. Also, tiles in S1 won't visually update themselves until you scroll away and back. This is solved in S2 by setting the "dirty flag", but I can't find any such flag in S1. Any ideas?
upthorn
He/Him
Active player, Emulator Coder (387)
Joined: 3/24/2006
Posts: 1802
Neither S1 nor S3K have a "dirty" screen flag, so what the camhack does there is make a savestate, set the camera values to one screen above/left of the target position, then invisibly emulate the number of frames required to scroll down/right onto the target (advancing the camera position by 16 pixels each frame), emulate one frame visibly, then load that savestate without redrawing the screen. As for S3K's specialized method of getting tile addresses, it's fairly simple once you understand it. There's a table of 16-bit pointers to the start of each row, starting at FF8008, and alternating foreground and background. So, to get a specific foreground tile
  1. Take its row number (Y/128), multiplied by 4 (so really just take Y/32)
  2. Read the short pointer at 0xFF8008 + that number
  3. Sign extend the short pointer (>= 0x8000 -> >= 0xFFFF8000, <= 7FFF -> <= 0x00007FFF)
  4. Add its column number (X/128) to the pointer
or, in lua change the beginning of work_tiles to
      function get_tileS1S2(X, Y)
              return addr_levellayout + ((Y * level_rowlength) * 2) + X
      end
      
      function get_tileS3SK(X, Y)
              local rowpoint, tilenum
              Y = AND(Y*4,memory.readword(0xFFEEAE)) --optional, for correct behavior when outside normal level height bounds
              rowpoint = memory.readword(addr_levellayout + Y + 8)
              if rowpoint > 0x7FFF then 
                              rowpoint = OR(rowpoint,0xFF0000)
              end
              tilenum = rowpoint + X
              return tilenum
      end
      
      
      function work_tiles(xcamera, ycamera)
              local xtile1, ytile1, tilepos, tilenum, x1, y1, x2, y2, get_tile
       
              if game == "s3sk" then
                              get_tile = get_tileS3SK
              else
                              get_tile = get_tileS1S2
              end
              xtile1 = math.floor((xcamera + xmouse) / level_tilesize)
              ytile1 = math.floor((ycamera + ymouse) / level_tilesize)
              tilepos = get_tile(xtile1,ytile1)
              tilenum = memory.readbyte(tilepos)
How fleeting are all human passions compared with the massive continuity of ducks.
Joined: 3/15/2009
Posts: 8
Thanks for the insight! S3/K tile editing works fine now, but I still can't quite fix the redraw problem. The problem with saving a state, doing invisible frames, etc, is that from Lua, Gens doesn't allow you to load a state without redrawing the screen. So if I were to load a state afterwards, it'd just bring me back to where I started (if I'm understanding it correctly). Also, some unrelated suggestions for Gens' Lua support because I'm not sure where else to post it: - We have the ability to get mouse clicks, but no way to get mousewheel activity. Maybe a couple more fields in the input.get() table for wheelup and wheeldown. - And perhaps hotkey status added to input.get() as well; so that there's a way to check for hotkeys without using callbacks. - Could it be possible for scripts to extend the menubar? It could go something like this:
gens.registermenu("NewMenu/SubMenu/MenuItem", function() end)
- Having multiple Lua windows is fine, but if it were instead a tabbed dialog, it'd save both screen space and taskbar space. ;)
Emulator Coder, Skilled player (1299)
Joined: 12/21/2004
Posts: 2687
deltaphc wrote:
from Lua, Gens doesn't allow you to load a state without redrawing the screen. So if I were to load a state afterwards, it'd just bring me back to where I started (if I'm understanding it correctly).
Loading an anonymous savestate does not redraw the screen. (If this is not true for you, then it's a bug that I have never seen happen, and I could use an example to reproduce it.) Furthermore, loading non-anomyous savestates can also be forced to not redraw the screen by using speedmode("maximum"). EDIT: In case anyone is confused about this and would like to become either more or less confused, you might want to read this.
upthorn
He/Him
Active player, Emulator Coder (387)
Joined: 3/24/2006
Posts: 1802
nitsuja wrote:
deltaphc wrote:
from Lua, Gens doesn't allow you to load a state without redrawing the screen. So if I were to load a state afterwards, it'd just bring me back to where I started (if I'm understanding it correctly).
Loading an anonymous savestate does not redraw the screen. (If this is not true for you, then it's a bug that I have never seen happen, and I could use an example to reproduce it.) Furthermore, loading non-anomyous savestates can also be forced to not redraw the screen by using speedmode("maximum").
deltaphc's script here is actually the precise example that caused me to make that issue report...
How fleeting are all human passions compared with the massive continuity of ducks.
Joined: 3/15/2009
Posts: 8
Been a little while since I last posted. http://gens-lua.pastebin.com/f6da2e320 What's changed: - Fancy new menu system. - '+' menu gives you contextual options depending on which Mode you're in. - Added ability to duplicate and delete objects. - Added new 'Game' mode, where you can modify various variables, restart the level, toggle level select/debug, and Go Super. - Added ability to dump level layout to a file. Useful if you're into Sonic hacking. - Tiles are now changed using left/right mouse buttons. - Other little tweaks and polish. ---- Edit by FractalFusion: Removed an image that was oddly out of place and oversized. It is linked here: http://imgur.com/A2XPJ.png
marzojr
He/Him
Experienced player (742)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
For the S3&K "go super" button: I made a script demonstrating how to make the Super/Hyper transformations (it is here). It is even possible to make a "Turbo Tails" transformation with only the seven chaos emeralds (it is off by default in the script I posted). You are free to incorporate it in your script if you wish.
Marzo Junior