Posts for marzojr

marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
Bobo the King wrote:
I checked the published run for the zips it used. Of the 18 levels, 12 used zips.
For what is worth, one of the reasons Aglar has decided to do a non-zip run is that there are several new zips which will be added to the normal run: the 3 Marble zone levels basically are all skipped due to new zips, there is a new zip/shortcut in Spring Yard 3 and Star Light 1 is basically skipped also due to a new zip.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
Brandon wrote:
The Scrap Brain glitches look nothing like zipping and I accept them as legitimate, but at the same time, you are skipping most of the level. Is there no goal we could use that forces you to take "a" normal route without being too arbitrary?
In both Scrap Brain 2 and 3, the taken routes are very close to the "normal" routes -- there is a rather minor skip ahead to another point of a "normal" route. In Scrap Brain 2, Aglar would end up on the exact same spot a few seconds later had he not used the crusher to go through the floor. The Scrap Brain 3 route skips a segment about 60-80% of the length of the skipped section in Scrap Brain 2; but this segment is mostly underwater, so quite a bit slower.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
I can't believe it took me this long to notice that the run had been submitted. It is an easy yes vote for me: it is a great show of TASing skill by Aglar, resulting in a great run. In the discussion regarding the restriction -- the run sticks to its restriction as it is defined, and Aglar made sure of that. When he was in doubt, he asked for a second opinion (mine); I watched those sections closely (technically, I watched the whole run in frame advance...) and tried a few things to verify whether or not a zip was taking place. The end result: a cool glitch he found on Star Light 2 was scrapped because it was a zip... but others remained because they are not zips. In my opinion, the definition he chose for what constitutes a zip is sound; and far less controversial than the one I used on the S3&K 100% run. But the most important bit: the run is not labeled as a 'glitchless' or 'low glitch' run -- it is, instead, a 'no zips' run. Moreover, I find it funny that people complain a lot about Scrap Brain 2 and 3, but don't bat an eye at Green Hill 2, for example, where the same thing happens at a smaller scale.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
upthorn wrote:
This would be the pillar that Robotnik appears in. The RNG seed also increments by 1 whenever sonic is in contact with final zone boss Robotnik's hitbox.
Ah, yes, I had forgotten that; you had told me this back when I was doing my first version of the Tails TAS.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
I can take care of it, yeah; I haven't had time to update the script from the last version yet, but I don't think it will actually be necessary. Now, this game is pretty stable regarding randomness: there are very few things* that use the actual RNG, most using instead the in-level frame counter or the v-int counter. I would say that the safest option would be to edit each zone before moving on to the next zone but, except for the animal patterns, I can see very little odds of things going wrong before Final zone. Just a question -- since you mentioned a "no zip" run, I would guess that would entail not zipping in Green Hill zone 2 (or risk the run being rejected for failing to adhere to its own goals). You will be doing that? (and yes, I know that deciding whether or not to use zips is a pain; I have been there...) Also: I imagine you plan on hex-editing the end-of-acts later? *: Specifically, the lava balls created by the Marble zone boss, the plasma balls in the Final zone boss, something else I can't identify in the Final Zone boss, the animal and explosion patterns in capsules, the animals freed from badniks, boss explosions, bubble movement and generation and the drowning countdown. Edit: As if to prove my point, when I spliced the better animal pattern, Marble zone 2 desynched. I still have to look into why, as I simply copied/pasted both Marble zone levels with my resyncher script (which, to be fair, only handles differences in lag). Edit 2: the difference was at the bat at around 6:00 -- he is too far forward and causes slight differences in subpixel position that propagate. By about 8:00, Sonic is half a pixel further left (and with a different subpixel y position as well) compared to the last WIP you posted. Here is the spliced movie (with all input past the point of desynch still left).
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
WarAnvil wrote:
For shame, guys. No Sonic 2 Heroes love? WIP up to EHZ, v.06. And just so you know, I do use two simultaneous controllers at times.
While I appreciate the sentiment, you are just opening yourself to a world of hurt when the next update (or the one after it) causes your run to desynch. I am still developing the hack, and while I am handling some sound driver stuff at the moment, I will be adding things such as save games and other teams. There is also some secret stuff I am working on which, when complete, will basically invalidate all movies. As for the movie itself: it is clearly suboptimal. There is no reason that you should not be able to match or beat the times from the published S2 run in this hack -- and that without abusing flight/glide. the Sega screen and title screen are both suboptimal; in this hack, you can just hold start at the Sega screen and it will be skipped at the first frame of the Sega sound. At the title screen you can press start way earlier, and you can press start in 1 controller then press start on the second (or third) controller on the next frame to start the game -- like is done in the S2 run. In EHZ1, you can also start the initial peelout some four frames earlier. It is also suboptimal -- I can get almost sub-21 unassisted after about 10 tries (without using Tails/Knuckles shenanigans to glide over the level), and I am not even that good unassisted. EHZ2 is so suboptimal I won't bother analyzing it.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
I volunteer for the hexing. Or I could simply release my copy/paste level script for it...
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
After converting the functions from ASM to lua and debugging, it turns out that the predictor is not good at estimating the exact time when an animal leaves the screen ("exit prediction") except for the initial batch of animals or for when the animal is on the move. One reason I can for this is object placement: animals loaded in slots after the capsule will load one frame earlier than those loaded before the capsule; this 1-frame difference can change the direction that the animal will travel, and this will affect exit time. Another factor (which I account for) is that an animal loaded after the capsule will take one extra frame to register as missing. I have no idea what other factors are at play; but the fact is that the exit prediction is off by up to plus/minus 3 frames in my tests. The wait until the animals are spawned/start moving works perfectly, only the endpoint prediction that fails -- unless the animal is already on the move. The only ways out of this are simulating the object placement or emulating all frames until the animals leave. I be looking into the latter; in any case, here is the current version of the script: Download predict-animals-v2.lua
Language: lua

local v_objspace = 0xFFFFD000 local c_objsize = 0x40 -- Object SST constants. local obRender = 0x1 local obX = 0x8 local obY = 0xC local obVelX = 0x10 local obVelY = 0x12 local obFrame = 0x1A local obHeight = 0x16 local obWidth = 0x17 local obTimeFrame = 0x1E local obRoutine = 0x24 -- A few important RAM locations. local v_random = 0xFFFFF636 local v_zone = 0xFFFFFE10 local v_vbla_byte = 0xFFFFFE0F local v_collindex = 0xFFFFF796 local v_lvllayout = 0xFFFFA400 local AngleMap = 0x00062900 local CollArray1 = 0x00062A00 -- Function to find the capsule'e switch local function find_capsule() for addr=v_objspace + c_objsize,v_objspace + 0x40 * c_objsize,c_objsize do if memory.readbyte(addr) == 0x3e and memory.readbyte(addr+obRoutine) > 2 then return addr end end return nil end -- Function to enumerate all animals that matter for the capsule local function enum_animals() local animals = {} for addr=v_objspace + c_objsize,v_objspace + 0x40 * c_objsize,c_objsize do if memory.readbyte(addr)==0x28 and memory.readbyte(addr+obRoutine) > 0 then table.insert(animals, addr) end end return animals end local function speed2color(speed, min, max) local green = math.floor(((speed - min) * 255) / (max - min)) local red = math.floor(((max - speed) * 255) / (max - min)) local blue, alpha = 0, 255 return {red, green, blue, alpha} end local minspd, maxspd = 0x140, 0x300 -- Names for animals. local animals = {[0]={"Rabbit" , speed2color(0x200, minspd, maxspd), -0x200, -0x400}, [1]={"Chicken" , speed2color(0x200, minspd, maxspd), -0x200, -0x300}, [2]={"Eagle" , speed2color(0x180, minspd, maxspd), -0x180, -0x300}, [3]={"Seal" , speed2color(0x140, minspd, maxspd), -0x140, -0x180}, [4]={"Pig" , speed2color(0x1C0, minspd, maxspd), -0x1C0, -0x300}, [5]={"Flicky" , speed2color(0x300, minspd, maxspd), -0x300, -0x400}, [6]={"Squirrel", speed2color(0x280, minspd, maxspd), -0x280, -0x380}} -- Map between zone ID and animal names. local typelist = {[0]={[0]=0, [1]=5}, [1]={[0]=2, [1]=3}, [2]={[0]=6, [1]=3}, [3]={[0]=4, [1]=5}, [4]={[0]=4, [1]=1}, [5]={[0]=0, [1]=1}} -- Adddress of capsule switch in RAM. local capsule_switch = nil -- Internal variables. local seed = 0 local last_seed = -1 local last_vbla = -1 -- Lua version of the Sonic 1 random number generator. local function GetRandom() local d1 = seed or 0x2A6D365A local d0 = d1 -- move.l d1,d0 d1 = SHIFT(d1, -2) -- asl.l #2,d1 d1 = d1 + d0 -- add.l d0,d1 d1 = SHIFT(d1, -3) -- asl.l #3,d1 d1 = d1 + d0 -- add.l d0,d1 d0 = OR(AND(d1, 0x0000FFFF), AND(d0, 0xFFFF0000)) -- move.w d1,d0 d1 = OR(SHIFT(AND(d1, 0x0000FFFF), -16), SHIFT(AND(d1, 0xFFFF0000), 16)) -- swap d1 d0 = OR(AND(d1 + d0, 0x0000FFFF), AND(d0, 0xFFFF0000)) -- add.w d1,d0 d1 = OR(SHIFT(AND(d0, 0x0000FFFF), -16), SHIFT(AND(d1, 0xFFFF0000), 16)) -- move.w d0,d1 \n swap d1 seed = d1 -- move.l d1,(v_random).w return d0 end -- Lua version of the Sonic 1 function to find the nearest tile. local function FindNearestTile(objRender, objX, objY) -- move.w d2,d0 -- lsr.w #1,d0 -- andi.w #$380,d0 -- move.w d3,d1 -- lsr.w #8,d1 -- andi.w #$7F,d1 -- add.w d1,d0 local index = AND(SHIFT(objY, 1), 0x380) + AND(SHIFT(objX, 8), 0x7F) -- Later: moveq #-1,d1 local tilenum = memory.readbyte(v_lvllayout + index) -- lea (v_lvllayout).w,a1 \n move.b (a1,d0.w),d1 if AND(tilenum, 0x80) ~= 0 then -- bmi.s @specialtile -- @specialtile: tilenum = AND(tilenum, 0x7F) -- andi.w #$7F,d1 if AND(objRender, 0x40) ~= 0 then -- btst #6,obRender(a0) \n beq.s @treatasnormal tilenum = tilenum + 1 -- addq.w #1,d1 if tilenum == 0x29 then -- cmpi.w #$29,d1 \n bne.s @treatasnormal tilenum = 0x51 -- move.w #$51,d1 end end -- @treatasnormal: tilenum = AND(tilenum - 1, 0xFF) -- subq.b #1,d1 if AND(tilenum, 0x80) ~= 0 then -- ext.w d1 tilenum = OR(tilenum, 0xFF00) end tilenum = OR(SHIFT(AND(tilenum, 0x7F), -9), SHIFT(AND(tilenum, 0xFF80), 7)) -- ror.w #7,d1 -- move.w d2,d0 -- add.w d0,d0 -- andi.w #$1E0,d0 -- add.w d0,d1 -- move.w d3,d0 -- lsr.w #3,d0 -- andi.w #$1E,d0 -- add.w d0,d1 return OR(AND(tilenum + AND(2 * objY, 0x1E0) + AND(SHIFT(objX, 3), 0x1E), 0xFFFF), 0xFFFF0000) end if tilenum > 0 then tilenum = AND(tilenum - 1, 0xFF) -- subq.b #1,d1 if AND(tilenum, 0x80) ~= 0 then -- ext.w d1 tilenum = OR(tilenum, 0xFF00) end tilenum = OR(SHIFT(AND(tilenum, 0x7F), -9), SHIFT(AND(tilenum, 0xFF80), 7)) -- ror.w #7,d1 -- move.w d2,d0 -- add.w d0,d0 -- andi.w #$1E0,d0 -- add.w d0,d1 -- move.w d3,d0 -- lsr.w #3,d0 -- andi.w #$1E,d0 -- add.w d0,d1 return OR(AND(tilenum + AND(2 * objY, 0x1E0) + AND(SHIFT(objX, 3), 0x1E), 0xFFFF), 0xFFFF0000) end -- @blanktile: return 0xFFFFFF00 -- movea.l d1,a1 \n rts end -- Lua version of the Sonic 1 secondary function to find the floor. local function FindFloor2(objRender, objX, objY, solidbit, delta, initangle, flipmask) local angle = initangle local tileaddr = FindNearestTile(objRender, objX, objY) -- bsr.s FindNearestTile local floordist = 0 local tile = memory.readword(tileaddr) -- move.w (a1),d0 \n move.w d0,d4 -- andi.w #$7FF,d0 \n beq.s @isblank2 \n btst d5,d4 \n bne.s @issolid local tileid = AND(tile, 0x7FF) if tileid ~= 0 and AND(tile, solidbit) ~= 0 then -- @issolid: local colptr = memory.readlong(v_collindex) -- movea.l (v_collindex).w,a2 local block = memory.readbyte(colptr + tileid) -- move.b (a2,d0.w),d0 if block == 0 then -- beq.s @isblank2 -- @isblank2 return 0xF - AND(objY, 0xF), tileaddr, angle end angle = memory.readbytesigned(AngleMap + block) -- lea (AngleMap).l,a2 \n move.b (a2,d0.w),(a4) block = SHIFT(block, -4) -- lsl.w #4,d0 local xcopy = objX -- move.w d3,d1 if AND(tile, BIT(0xB)) ~= 0 then -- btst #$B,d4 \n beq.s @noflip xcopy = -objX-1 -- not.w d1 angle = -angle -- neg.b (a4) end -- @noflip: if AND(tile, BIT(0xC)) ~= 0 then --- btst #$C,d4 \n beq.s @noflip2 angle = -0x80 - angle -- addi.b #$40,(a4) \n neg.b (a4) \n subi.b #$40,(a4) end -- @noflip2: xcopy = AND(objX, 0xF) + block -- andi.w #$F,d1 \n add.w d0,d1 local colhgt = memory.readbytesigned(CollArray1 + xcopy) -- lea (CollArray1).l,a2 \n move.b (a2,d1.w),d0 \n ext.w d0 tile = XOR(tile, flipmask) -- eor.w d6,d4 if AND(tile, BIT(0xC)) ~= 0 then -- btst #$C,d4 \n beq.s @noflip3 colhgt = -colhgt -- neg.w d0 end -- @noflip3: if colhgt == 0 then -- tst.w d0 \n beq.s @isblank2 -- @isblank2 return 0xF - AND(objY, 0xF), tileaddr, angle elseif colhgt < 0 then -- bmi.s @negfloor -- @negfloor: local dist = AND(objY, 0xF) -- move.w d2,d1 \n andi.w #$F,d1 if dist + colhgt < 0 then -- add.w d1,d0 \n bpl.w @isblank2 return -dist-1, tileaddr, angle -- not.w d1 \n rts end else local dist = AND(objY, 0xF) -- move.w d2,d1 \n andi.w #$F,d1 return 0xF - (dist + colhgt), tileaddr, angle -- add.w d1,d0 \n move.w #$F,d1 \n sub.w d0,d1 \n rts end end -- @isblank2: -- move.w #$F,d1 -- move.w d2,d0 -- andi.w #$F,d0 -- sub.w d0,d1 -- rts return 0xF - AND(objY, 0xF), tileaddr, angle end -- Lua version of the Sonic 1 function to find the floor. local function FindFloor(objRender, objX, objY, solidbit, delta, initangle, flipmask) local angle = initangle local tileaddr = FindNearestTile(objRender, objX, objY) -- bsr.s FindNearestTile local floordist = 0 local tile = memory.readword(tileaddr) -- move.w (a1),d0 \n move.w d0,d4 -- andi.w #$7FF,d0 \n beq.s @isblank \n btst d5,d4 \n bne.s @issolid local tileid = AND(tile, 0x7FF) if tileid ~= 0 and AND(tile, solidbit) ~= 0 then -- @issolid: local colptr = memory.readlong(v_collindex) -- movea.l (v_collindex).w,a2 local block = memory.readbyte(colptr + tileid) -- move.b (a2,d0.w),d0 if block == 0 then -- beq.s @isblank -- @isblank floordist, tileaddr, angle = FindFloor2(objRender, objX, objY + delta, solidbit, delta, initangle, flipmask) return floordist + 0x10, tileaddr, angle end angle = memory.readbytesigned(AngleMap + block) -- lea (AngleMap).l,a2 \n move.b (a2,d0.w),(a4) block = SHIFT(block, -4) -- lsl.w #4,d0 local xcopy = objX -- move.w d3,d1 if AND(tile, BIT(0xB)) ~= 0 then -- btst #$B,d4 \n beq.s @noflip xcopy = -objX-1 -- not.w d1 angle = -angle -- neg.b (a4) end -- @noflip: if AND(tile, BIT(0xC)) ~= 0 then --- btst #$C,d4 \n beq.s @noflip2 angle = -0x80 - angle -- addi.b #$40,(a4) \n neg.b (a4) \n subi.b #$40,(a4) end -- @noflip2: xcopy = AND(objX, 0xF) + block -- andi.w #$F,d1 \n add.w d0,d1 local colhgt = memory.readbytesigned(CollArray1 + xcopy) -- lea (CollArray1).l,a2 \n move.b (a2,d1.w),d0 \n ext.w d0 tile = XOR(tile, flipmask) -- eor.w d6,d4 if AND(tile, BIT(0xC)) ~= 0 then -- btst #$C,d4 \n beq.s @noflip3 colhgt = -colhgt -- neg.w d0 end -- @noflip3: if colhgt == 0 then -- tst.w d0 \n beq.s @isblank -- @isblank floordist, tileaddr, angle = FindFloor2(objRender, objX, objY + delta, solidbit, delta, initangle, flipmask) return floordist + 0x10, tileaddr, angle elseif colhgt < 0 then -- bmi.s @negfloor -- @negfloor: local dist = AND(objY, 0xF) -- move.w d2,d1 \n andi.w #$F,d1 if dist + colhgt < 0 then -- add.w d1,d0 \n bpl.w @isblank -- @maxfloor floordist, tileaddr, angle = FindFloor2(objRender, objX, objY - delta, solidbit, delta, initangle, flipmask) return floordist - 0x10, tileaddr, angle end elseif colhgt == 0x10 then -- cmpi.b #$10,d0 \n beq.s @maxfloor -- @maxfloor: -- sub.w a3,d2 -- bsr.w FindFloor2 -- add.w a3,d2 -- subi.w #$10,d1 -- rts floordist, tileaddr, angle = FindFloor2(objRender, objX, objY - delta, solidbit, delta, initangle, flipmask) return floordist - 0x10, tileaddr, angle else -- move.w d2,d1 -- andi.w #$F,d1 -- add.w d1,d0 -- move.w #$F,d1 -- sub.w d0,d1 -- rts return 0xF - (AND(objY, 0xF) + colhgt), tileaddr, angle end end -- @isblank: -- add.w a3,d2 -- bsr.w FindFloor2 ; try tile below the nearest -- sub.w a3,d2 -- addi.w #$10,d1 ; return distance to floor -- rts floordist, tileaddr, angle = FindFloor2(objRender, objX, objY + delta, solidbit, delta, initangle, flipmask) -- beq.s @isblank return floordist + 0x10, tileaddr, angle end -- Lua version of the Sonic 1 function to find distance to the floor. local function ObjFloorDist(objX, objY, objH, objR) --local objX = memory.readword(obj+obX) -- move.w obX(a0),d3 --local objY = memory.readword(obj+obY) -- move.w obY(a0),d2 --local objH = memory.readbytesigned(obj+obHeight) -- moveq #0,d0 \n move.b obHeight(a0),d0 \n ext.w d0 --local objR = memory.readbyte(obj+obRender) --objY = objY + objH -- add.w d0,d2 -- lea (v_anglebuffer).w,a4 -- move.b #0,(a4) -- movea.w #$10,a3 -- move.w #0,d6 -- moveq #$D,d5 local floordist, tileaddr, angle = FindFloor(objR, objX, objY + objH, BIT(0xD), 0x10, 0, 0) -- bsr.w FindFloor if AND(angle, 1) ~= 0 then angle = 0 -- move.b (v_anglebuffer).w,d3 \n btst #0,d3 \n beq.s locret_14E4E \n move.b #0,d3 end return floordist, tileaddr, angle -- locret_14E4E: rts end -- Lua version of the Sonic 1 function to make an object "fall". local function ObjectFall(objX, objY, objVX, objVY) --local memory.readlong(obj+obX) -- move.l obX(a0),d2 --local memory.readlong(obj+obY) -- move.l obY(a0),d3 --local memory.readwordsigned(obj+obVelX) -- move.w obVelX(a0),d0 \n ext.l d0 objX = AND(objX + SHIFT(objVX, -8), 0xFFFFFFFF) -- asl.l #8,d0 \n add.l d0,d2 --local memory.readwordsigned(obj+obVelY) -- move.w obVelY(a0),d0 \n ext.l d0 objY = AND(objY + SHIFT(objVY, -8), 0xFFFFFFFF) -- asl.l #8,d0 \n add.l d0,d3 objVY = objVY + 0x38 -- addi.w #$38,obVelY(a0) return objX, objY, objVX, objVY -- move.l d2,obX(a0) \n move.l d3,obY(a0) \n rts end -- Lua version of the Sonic 1 function loc_912A. local function animal_fall(objX, objY, objVX, objVY, objH, objR, objNVX, objNVY, vbla) -- tst.b obRender(a0) -- bpl.w DeleteObject objX, objY, objVX, objVY = ObjectFall(objX, objY, objVX, objVY) -- bsr.w ObjectFall if objVY >= 0 then -- tst.w obVelY(a0) \n bmi.s loc_9180 local floordist = ObjFloorDist(SHIFT(objX, 16), SHIFT(objY, 16), objH, objR) -- jsr ObjFloorDist if floordist < 0 then -- tst.w d1 \n bpl.s loc_9180 objY = objY + floordist -- add.w d1,obY(a0) objVX = objNVX -- move.w $32(a0),obVelX(a0) objVY = objNVY -- move.w $34(a0),obVelY(a0) -- move.b #1,obFrame(a0) -- move.b $30(a0),d0 -- add.b d0,d0 -- addq.b #4,d0 -- move.b d0,obRoutine(a0) -- tst.b (v_bossstatus).w -- beq.s loc_9180 if AND(vbla, BIT(4)) ~= 0 then -- btst #4,(v_vbla_byte).w \n beq.s loc_9180 objVX = -objVX -- neg.w obVelX(a0) objR = XOR(objR, BIT(0)) -- bchg #0,obRender(a0) end end end -- loc_9180: -- bra.w DisplaySprite return objX, objY, objVX, objVY, objH, objR, objNVX, objNVY, vbla end local function simulate_animal_delay_fall_escape(obj, objX, objY, objVX, objVY, objNVX, objNVY, objWait, vbla, capX) local objH = 0xC local objR = 0x85 local objW = 8 local initvbla = vbla -- If we have a real object: if obj ~= nil then objX = memory.readlong(obj+obX) objY = memory.readlong(obj+obY) objVX = memory.readwordsigned(obj+obVelX) objVY = memory.readwordsigned(obj+obVelY) objNVX = memory.readwordsigned(obj+0x32) objNVY = memory.readwordsigned(obj+0x34) objWait = memory.readword(obj+0x36) - 1 end -- Simulate wait if needed if objWait > 0 then vbla = vbla + objWait + 1 end -- Simulate falling while objVX == 0 do vbla = vbla + 1 objX, objY, objVX, objVY, objH, objR, objNVX, objNVY, vbla = animal_fall(objX, objY, objVX, objVY, objH, objR, objNVX, objNVY, vbla) end -- Compute time to get offscreen local le = SHIFT(capX - 160 - objW, -8) local re = SHIFT(capX + 160 + objW, -8) -- Lets use relative position here. objX = SHIFT(objX, 8) local lastvbla = vbla if objVX > 0 then vbla = vbla + math.abs(math.floor((re - objX + objVX - 1) / objVX)) + 1 else vbla = vbla + math.abs(math.floor((objX - le - objVX - 1) / -objVX)) + 1 end return objVX, vbla end function print_animal_objects(vbla) local animal_list = enum_animals() if #animal_list > 0 then row = 4 gui.drawtext(4, row, "Spawned animals:") row = row + 8 gui.drawtext(12, row, "Animal Wait D Exit at") row = row + 8 local capX = memory.readword(capsule_switch+obX) local list1 = {} local list2 = {} for i,m in pairs(animal_list) do local ty = memory.readbyte(m+0x30) local delay = memory.readword(m+0x36) local vx, endvbla = simulate_animal_delay_fall_escape(m, nil, nil, nil, nil, nil, nil, nil, vbla, capX) local dir = (vx > 0 and ">") or "<" local endpt = movie.framecount() + endvbla - vbla + 1 table.insert((delay == 0 and list2) or list1, {endpt, function(rw) gui.drawtext(12, rw, string.format("%-8s %4d %s %7d", animals[ty][1], delay, dir, endpt), animals[ty][2]) end}) end table.sort(list1, function(item1, item2) return item1[1] < item2[1] end) table.sort(list2, function(item1, item2) return item1[1] < item2[1] end) for i,m in pairs(list1) do m[2](row) row = row + 8 end gui.drawtext(12, row, "========= Free =========") row = row + 8 for i,m in pairs(list2) do m[2](row) row = row + 8 end end end function predict_animals() -- Try to find a capsule switch capsule_switch = find_capsule() if capsule_switch == nil then -- If it was not found, so update variables and leave last_seed = memory.readlong(v_random) last_vbla = memory.readbyte(v_vbla_byte) return end -- Update random seed to RAM value seed = memory.readlong(v_random) -- Capsule switch's routine counter local rout = memory.readbyte(capsule_switch+obRoutine) -- V-Int counter, used for pseudo-random numbers local vbla = memory.readbyte(v_vbla_byte) -- Sometimes, the V-Int counter does not update; doing this "manual" -- update corrects the issue, which causes misprediction. if last_vbla == vbla then vbla = vbla + 1 end local savevbla = vbla -- Special case: if rout == 4 then -- Switch not pressed gui.drawtext(4, 4, "Capsule not broken yet.") last_seed = seed last_vbla = savevbla return end local row = 0 -- Draw container box. gui.drawbox(0, 0, 223, 28*8-1, { 0, 0, 0, 128}, {255, 255, 255, 255}) if rout == 0xE then -- All animals spawned and moving to leave screen gui.drawtext(128, 12, "Waiting for animals\nto leave screen.", {255, 255, 0, 255}) print_animal_objects(vbla) last_seed = seed last_vbla = savevbla return end -- Get animal list for current zone. local zone = memory.readbyte(v_zone) local types = typelist[zone] if rout <= 0xA then -- Explosions are being generated. There are two parts to this: -- Part 1: counting the explosions local explosions_left = 0 -- Time left for explosion generation local countdown = memory.readword(capsule_switch + obTimeFrame) local dt = 7 - AND(vbla, 7) for i = 1, countdown + 1 do -- Has an explosion been generated? if AND(vbla + i - 1, 7) == 0 then -- If yes, we need to account for it: each explosion needs -- one random number. explosions_left = explosions_left + 1 local rand = GetRandom() end -- Fake frame advance. end -- If we have explosions left, we have opportunities for manipulating -- the animal pattern. if explosions_left > 0 then gui.drawtext(112, 180, string.format("Next oportunity to change\nanimal pattern: %d frame%s", dt, (dt == 1 and "") or "s"), {255, 255, 0, 255}) end row = 4 gui.drawtext(4, row, string.format("Explosions left: %d", explosions_left)) row = row + 16 gui.drawtext(4, row, "Initial animals:") row = row + 8 gui.drawtext(12, row, "Animal Wait D Exit at") row = row + 8 -- Build sequence of random numbers to be used for the initial batch -- of animals. Do this so we can print them in ascending order of delay. local randseq = {} for i = 1, 8 do local rand = GetRandom() table.insert(randseq, 1, rand) end local capX = memory.readword(capsule_switch+obX) local capY = memory.readword(capsule_switch+obY) + 0x20 -- Part 2: Initial animals for i = 1, 8 do -- Random number determines kind of animal. local rand = randseq[1] table.remove(randseq, 1) local ty = types[AND(rand, 1)] local objX = SHIFT(capX + 0x1C - 7 * i, -16) local objY = SHIFT(capY, -16) local delay = countdown + 90 + 8 * i local vx, endvbla = simulate_animal_delay_fall_escape(nil, objX, objY, 0, -0x400, animals[ty][3], animals[ty][4], delay, vbla, capX) local dir = (vx > 0 and ">") or "<" gui.drawtext(12, row, string.format("%-8s %4d %s %7d", animals[ty][1], delay, dir, movie.framecount() + endvbla - vbla), animals[ty][2]) row = row + 8 end end print_animal_objects(vbla) if rout <= 0xC then -- Single animals are being generated. -- Part 3: Pseudo-random animal generation row = 4 local saverow = row row = row + 8 gui.drawtext(124, row, "Animal Spawn D Exit at") row = row + 8 local animals_left = 0 local countdown = (rout < 0xC and memory.readword(capsule_switch+obTimeFrame)) or 0 local dt = 7 - AND(vbla + countdown, 7) local timer = (rout == 0xC and memory.readword(capsule_switch+obTimeFrame)) or 150 local capX = memory.readword(capsule_switch+obX) local capY = memory.readword(capsule_switch+obY) + ((rout < 0xC and 0x20) or 0) for i = 1, timer + 1 do if AND(vbla + countdown + i - 1, 7) == 0 then -- First random number is used to determine the position -- offset from the switch. local rand = AND(GetRandom(), 0x1F) - 6 local pos = (seed > 0 and rand) or -rand -- Second random number determines animal type. rand = GetRandom() local ty = types[AND(rand, 1)] if countdown + i - 2 > 0 then local objX = SHIFT(capX + pos, -16) local objY = SHIFT(capY, -16) local delay = countdown + i + 12 local vx, endvbla = simulate_animal_delay_fall_escape(nil, objX, objY, 0, -0x400, animals[ty][3], animals[ty][4], delay, vbla, capX) local dir = (vx > 0 and ">") or "<" gui.drawtext(124, row, string.format("%-8s %4d %s %7d", animals[ty][1], delay-14, dir, movie.framecount() + endvbla - vbla + (rout < 0xC and 1 or 0)), animals[ty][2]) row = row + 8 animals_left = animals_left + 1 end end end -- If we have animals still to be generated, and we are not in the -- process of making explosions, we have an opportunity to change -- if the next animal will be loaded or not. if rout == 0xC and animals_left > 0 then gui.drawtext(112, 180, string.format("Next oportunity to prevent\nanimal spawn: %d frame%s", dt, (dt == 1 and "") or "s"), {255, 255, 0, 255}) end -- If there were any animals left, say so if animals_left > 0 then gui.drawtext(116, saverow, string.format("Animal spawns: %d left", animals_left)) end end last_seed = memory.readlong(v_random) last_vbla = savevbla return end gens.registerafter(predict_animals) savestate.registerload(predict_animals)
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
Yes, the initial animals must be manipulated all at once; they are all generated in the same frame (it can be either with the last explosion or up to 6 frames after it), the last ones being created first. For the new movie, I got the best results so far by taking the first 3 opportunities to manipulate the initial animals (frames 7484, 7493 and 7501) then letting it run with no further manipulation; this results in all animals being offscreen by frame 7804, with the last bunny going offscreen with the last flicky -- compared to frame 7829 without any manipulation (the old number (32 frames) was for the old movie). There are better manipulations possible (manipulating the initial animals for the first 6 times instead), but the added frame count for the pauses offsets the gains. Hm. Since I know the animal speeds, and can compute their initial positions, I could theoretically compute how long they would take to go offscreen; that would require simulating their movement out of the capsule so I know when they hit the ground (as terrain is somewhat uneven) to know when they start moving offscreen (so I know the direction they take). That would even allow me to compute (albeit approximately) the total time it takes. This will take some research, and would require converting some more functions from ASM to lua, so it will take some time.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
Patashu wrote:
I thought the sonic run focused on in-game time over real time, as evidenced by ignoring the huge amount of time tallying up a time bonus takes. Why can't we ignore this too?
Because it looks unpolished :-) Besides, the primary focus is in-game time, but that does not mean that real time can just be ignored; this is a TAS after all. Anyway: the predictor script is finished. Just load it when getting near a capsule (preferably just before hitting it). It displays the sequence of animals with timers until they are released; the information is only displayed until you can no longer change it anymore. The faster an animal is, the greener is his color; the slower said animal is, the redder. This is calibrated so that flickies (the fastest of them all) are full green while seals (the slowest of them all) are fully red. In yellow is a timer; it says how many frames before you can have an effect on the animal pattern (until the explosions are finished) or whether or not an animal will spawn (after the explosions are done). When the timer reads 1, you hold the start key and do a frame advance; then advance one frame without pressing start and then advance another frame unpausing. This will give a new pattern (if done while the explosions are still happening) or will delay the next animal spawn (delaying enough animal spawns will prevent the spawns; so if you get a near-perfect pattern except that it ends on a slow animal, you can force it not to spawn). You want to get fast animals (generally birds) in the last places in both lists. This generally more than makes up for the pauses that it takes. You can still try to manipulate the way an animal will go when he lands by adding pauses; this script does not cover this. You can manipulate this with pauses; it may take anywhere from 1 to 14 frames in a pause to convince an animal to change the direction he goes -- and it has to be done before he lands. Here is the script: Download predict-animals.lua
Language: lua

-- Function to find the capsule'e switch local function find_capsule() for addr=0xffd040,0xffd040+0x3f*0x40,0x40 do if memory.readbyte(addr)==0x3e and memory.readbyte(addr+0x24) > 2 then return addr end end return nil end -- Names for animals. local animals = {[0]={"Rabbit" ,{146, 109, 0, 255}}, [1]={"Chicken" ,{146, 109, 0, 255}}, [2]={"Eagle" ,{219, 36, 0, 255}}, [3]={"Seal" ,{255, 0, 0, 255}}, [4]={"Pig" ,{183, 72, 0, 255}}, [5]={"Flicky" ,{ 0, 255, 0, 255}}, [6]={"Squirrel",{ 73, 182, 0, 255}}} -- Map between zone ID and animal names. local typelist = {[0]={[0]=0, [1]=5}, [1]={[0]=2, [1]=3}, [2]={[0]=6, [1]=3}, [3]={[0]=4, [1]=5}, [4]={[0]=4, [1]=1}, [5]={[0]=0, [1]=1}} -- Adddress of capsule switch in RAM. local capsule_switch = nil -- A few important RAM locations. local v_random = 0xFFFFF636 local v_zone = 0xFFFFFE10 local v_vbla_byte = 0xFFFFFE0F -- Internal variables. local seed = 0 local last_seed = -1 local last_vbla = -1 -- Lua version of the Sonic 1 random number generator. local function GetRandom() local d1 = seed or 0x2A6D365A local d0 = d1 -- move.l d1,d0 d1 = SHIFT(d1, -2) -- asl.l #2,d1 d1 = d1 + d0 -- add.l d0,d1 d1 = SHIFT(d1, -3) -- asl.l #3,d1 d1 = d1 + d0 -- add.l d0,d1 d0 = OR(AND(d1, 0x0000FFFF), AND(d0, 0xFFFF0000)) -- move.w d1,d0 d1 = OR(SHIFT(AND(d1, 0x0000FFFF), -16), SHIFT(AND(d1, 0xFFFF0000), 16)) -- swap d1 d0 = OR(AND(d1 + d0, 0x0000FFFF), AND(d0, 0xFFFF0000)) -- add.w d1,d0 d1 = OR(SHIFT(AND(d0, 0x0000FFFF), -16), SHIFT(AND(d1, 0xFFFF0000), 16)) -- move.w d0,d1 \n swap d1 seed = d1 -- move.l d1,(v_random).w return d0 end function predict_animals() -- Try to find a capsule switch capsule_switch = find_capsule() if capsule_switch == nil then -- If it was not found, so update variables and leave last_seed = memory.readlong(v_random) last_vbla = memory.readbyte(v_vbla_byte) return end -- Update random seed to RAM value seed = memory.readlong(v_random) -- Capsule switch's routine counter local rout = memory.readbyte(capsule_switch+0x24) -- V-Int counter, used for pseudo-random numbers local vbla = memory.readbyte(v_vbla_byte) -- Special cases: if rout == 4 then -- Switch not pressed gui.drawtext(4, 4, "Capsule not broken yet.") last_seed = seed last_vbla = vbla return elseif rout == 0xE then -- All animals spawned and moving to leave screen gui.drawtext(4, 4, "Waiting for animals to leave screen.") last_seed = seed last_vbla = vbla return end -- Sometimes, the V-Int counter does not update; doing this "manual" -- update corrects the issue, which causes misprediction. if last_vbla == vbla then vbla = vbla + 1 end local row = 0 -- Draw container box. gui.drawbox(0, 0, 207, 21*8, { 0, 0, 0, 128}, {255, 255, 255, 255}) -- Get animal list for current zone. local zone = memory.readbyte(v_zone) local types = typelist[zone] local countdown = 0 if rout <= 0xA then -- Explosions are being generated. There are two parts to this: -- Part 1: counting the explosions local explosions_left = 0 -- Time left for explosion generation countdown = memory.readword(capsule_switch+0x1E) local dt = 7 - AND(vbla, 7) for i=1,countdown+1 do -- Has an explosion been generated? if AND(vbla, 7) == 0 then -- If yes, we need to account for it: each explosion needs -- one random number. explosions_left = explosions_left + 1 local rand = GetRandom() end -- Fake frame advance. vbla = vbla + 1 end -- If we have explosions left, we have opportunities for manipulating -- the animal pattern. if explosions_left > 0 then gui.drawtext(4, 132, string.format("Next oportunity to change\nanimal pattern: %d frame%s", dt, (dt == 1 and "") or "s"), {255, 255, 0, 255}) end row = 4 gui.drawtext(4, row, string.format("Explosions left: %d", explosions_left)) row = row + 16 gui.drawtext(4, row, "Initial animals:") row = row + 8 -- Build sequence of random numbers to be used for the initial batch -- of animals. Do this so we can print them in ascending order of delay. local randseq = {} for i=1,8 do local rand = GetRandom() table.insert(randseq, 1, rand) end -- Part 2: Initial animals for i=1,8 do -- Random number determines kind of animal. local rand = randseq[1] table.remove(randseq, 1) local ty = types[AND(rand, 1)] gui.drawtext(12, row, string.format("%-8s in %3d frames", animals[ty][1], countdown + 90 + 8*i), animals[ty][2]) row = row + 8 end end if rout <= 0xC then -- Single animals are being generated. -- Part 3: Pseudo-random animal generation row = 4 local saverow = row row = row + 8 local timer = (rout == 0xC and memory.readword(capsule_switch+0x1E)) or 150 local animals_left = 0 local dt = 7 - AND(vbla, 7) for i=1,timer+1 do if AND(vbla, 7) == 0 then -- First random number is used to determine the position -- offset from the switch. local rand = AND(GetRandom(), 0x1F) - 6 local pos = (seed > 0 and rand) or -rand -- Second random number determines animal type. rand = GetRandom() local ty = types[AND(rand, 1)] if countdown + i - 2 > 0 then gui.drawtext(116, row, string.format("%-8s in %3d frame%s", animals[ty][1], countdown + i - 2, ((countdown + i - 2) == 1 and "") or "s"), animals[ty][2]) row = row + 8 animals_left = animals_left + 1 end end vbla = vbla + 1 end -- If we have animals still to be generated, and we are not in the -- process of making explosions, we have an opportunity to change -- if the next animal will be loaded or not. if rout == 0xC and animals_left > 0 then gui.drawtext(4, 132, string.format("Next oportunity to prevent\nanimal spawn: %d frame%s", dt, (dt == 1 and "") or "s"), {255, 255, 0, 255}) end -- If there were any animals left, say so if animals_left > 0 then gui.drawtext(108, saverow, string.format("Animal spawns: %d left", animals_left)) end end last_seed = memory.readlong(v_random) last_vbla = memory.readbyte(v_vbla_byte) return end gens.registerafter(predict_animals) savestate.registerload(predict_animals)[/lua] With proper modifications, this could be used for Sonic 2 too.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
I am working on a better script that, instead of brute forcing, predicts what the animals are going to be given the current RNG. Right now, the script is limited to the initial 8 animals generated; I am still writing code to predict what each of the subsequent animals will be as they come. At its current state (i.e., only the initial 8 birds) I was already able to remove 32 frames (real time) with a few pauses by just picking the pauses that let me have the list with the highest proportion of birds in the slots with highest delay.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
So, I decided to go over the code and I found out a few things that may help. Manipulating early enough can alter the explosion pattern (if the last 3 bits of $FFFFFE0F are not zero, no explosion object is loaded*); otherwise, 1 explosion is loaded that frame. Each explosion loaded affects the RNG (used once per explosion to determine position of the explosion). This goes on as long as byte $FFD8A4** is $A. After 60 frames generating explosions, 8 animals will be generated in preset positions. These animals will start jumping 98 frames after this, and each will jump 8 frames after the other. Byte $FFD8A4** will change to $C and a 150 frame timer will be set. Edit: each of these animals use the RNG once to determine what type of animal they will be. During this time, animals are generated quasi-randomly: if the last 3 bits of $FFFFFE0F are not zero, no animal is loaded***; otherwise, 1 animal will be loaded that frame. Each animal uses the RNG once to determine its position, then once more to determine the type of animal. Each animal generated in this phase will jump 12 frames after being created. After the 150-frame timer expires, byte $FFD8A4** becomes $E and the capsule waits until all animals are offscreen. When it does, $FFD8A4** becomes 0 as the object checking it is deleted. Finally, as each animal land, byte bit 4 of $FFFFFE0F determines the direction that the animal goes, as I said earlier. So yeah, there is quite a bit to manipulate: you can turn the last bunnies into birds or even prevent them from loading with pauses in the right place. * Yes, it is possible to cause the capsule to explode without any explosions; it just suddenly appears broken after some time. ** This may change in different levels, because the object is dynamically loaded. If you use the Gens 11b with the Sonic TAS tools, turning numlock on will show the object's base address. In this case, you will look for the switch's address; it will show as something like $D880. Add $24 to it and put it in a RAM watch as $FFD8A4 to have the address to watch. While at it, the word at offset $1E from the object's RAM is the timer used between phases. *** It is possible to get down to 8 animals only, but this isn't all that useful -- most of them will get out much earlier than those generated afterward. But you can prevent the later ones from spawning; any bunnies that are created when the capsule timer is 142 or more are a waste.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
The script does use pause for manipulating the animals, and you started from about the place intended (which I now realize I forgot to mention... sorry). What is odd is that I have built-in checks to use the best time -- if that is without any pauses, then that is what the script should eventually use. Out of curiosity: what version of Gens are you using (just to rule out differences in Lua)? In any case, I will go about the code and see if I spot any mistakes. Also, something to try, just to be sure: after running the script, and while recording, could you try doing a manual savestate, advancing one frame then loading this state (another just in case)?
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
Here is the script. It is not terribly optimized, and does a lot by chance; it also does not save its history, so you will want to leave it running as long as you can afford -- for example, overnight. That said, it is a suitable baseline for a better script :-) There are two important parameters: "maxpauses" and "counter". "maxpauses" control how many pauses you want, and ranges from 1 to 16 in the script. Each pause can last from 2 to 16 frames, randomly chosen. "counter" controls how many tries with that maximum number of pauses before incrementing the number of pauses. I arbitrarily set "counter" to range from 0 to 256 (256 tries), but you can tweak that. At any point, you can press stop and it will load the best state found so far (i.e., the one where the animals are all off-screen at the earliest frame). Since it doesn't save its history, it is best to leave it running overnight and see what it came out with. Download optimize-animals.lua
Language: lua

local init_state = savestate.create() local best_state = savestate.create() local init_frame = movie.framecount() local best_frame = 0 -- Save initial state savestate.save(init_state) local iter = 29 local function redraw(msg) iter = iter + 1 if iter == 30 then iter = 0 gui.drawtext(80, 100, msg) gens.redraw() end end local function find_animals() for addr=0xffd040,0xffd040+0x3f*0x40,0x40 do if memory.readbyte(addr)==0x28 then return true end end return false end local function enum_animals() local animals = {} for addr=0xffd040,0xffd040+0x3f*0x40,0x40 do if memory.readbyte(addr)==0x28 then table.insert(animals, addr) end end return animals end local function check_animal_presence(list) local remove_queue = {} for i,m in pairs(list) do if memory.readbyte(m)==0 then table.insert(remove_queue, 1, i) end end local todel = remove_queue[1] while todel do table.remove(remove_queue, 1) table.remove(list, todel) todel = remove_queue[1] end return list end local function check_animal_states(list) for i,m in pairs(list) do local rout = memory.readbyte(m+0x24) if rout==0x12 or rout<=2 then return true end end return false end -- Estabilish baseline -- First, wait for animals to appear while true do gens.emulateframeinvisible() redraw("Baseline: Waiting for animals to appear...") if find_animals() then break end end gens.redraw() local animals = {} -- Now, wait for them to land while true do gens.emulateframeinvisible() redraw("Baseline: Waiting for animals to land...") animals = enum_animals() if not check_animal_states(animals) then break end end -- Now, wait for them to go all away while true do gens.emulateframeinvisible() redraw("Baseline: Waiting for animals to go away...") animals = check_animal_presence(animals) if #animals == 0 then break end end gens.redraw() -- Save baseline (current best) best_frame = movie.framecount() savestate.save(best_state) gens.registerexit(function() savestate.load(best_state) gens.emulateframe() savestate.load(best_state) gens.redraw() end) -- Rewind savestate.load(init_state) local npauses = 0 local pause_seed = 0 local pause_len = 2 local last_pause = init_frame local function add_pause(count, max) if npauses >= max then return end if pause_len ~= movie.framecount()-last_pause then return end joypad.write(1,{start=true}) for i=0,pause_len do gens.emulateframeinvisible() joypad.write(1,{start=false}) end joypad.write(1,{start=true}) gens.emulateframeinvisible() joypad.write(1,{start=false}) npauses = npauses+1 pause_seed = math.random(0,math.floor(256/max)) pause_len = math.random(2,16) last_pause = movie.framecount() end for maxpauses=1,16 do local animals = {} local state = 0 -- 0: waiting for animals to spawn; 1: waiting for animals to land; 2: waiting for animals to go away for counter=0,256 do npauses = 0 last_pause = init_frame local msg = string.format("Please wait... 0x%04X", counter) add_pause(counter, maxpauses) -- First, wait for animals to appear while state == 0 do gens.emulateframeinvisible() redraw(msg) if find_animals() then state = 1 break end add_pause(counter, maxpauses) end -- Now, wait for them to land while state == 1 do gens.emulateframeinvisible() redraw(msg) animals = enum_animals() if not check_animal_states(animals) then state = 2 break end add_pause(counter, maxpauses) end -- Now, wait for them to go all away while state == 2 do gens.emulateframeinvisible() redraw(msg) animals = check_animal_presence(animals) if #animals == 0 then state = 0 break end end gens.redraw() if best_frame > movie.framecount() then -- Save best best_frame = movie.framecount() savestate.save(best_state) end -- Rewind savestate.load(init_state) end end savestate.load(best_state)
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
You can also add small pauses to manipulate the animals; randomness there (the direction where each animal faces) is determined by how many times the v-int code has executed when the animal lands on ground. You want to watch the byte at $FFFFFE0F; if bit 4 is set (bits go 0-7), then the animal will go right; otherwise, it goes left. In your case, the problem comes from a pair of bunnies at the end that go to the wrong direction and cost you time. I will be writing a script to optimize it; but on your case, if you do a pause on frame 7481->7482 and release on 7485->7486, then pause again on 7730->7731 and release on 7732->7733, the bunnies will turn on a better way and the end of act will trigger 14 frames earlier. There may be a better combination, I don't know yet. Edit: I almost forgot: that is a nice job there. You made me smile in a couple points.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
I would say that up to Labyrinth zone the game is hex friendly; then, after Labyrinth zone (but before Scrap Brain zone 3), the game is hex friendly again; then Final zone is hex friendly. Labyrinth zone and Scrap Brain zone 3 have water, and this causes differences in lag that can cause desynchs. If you are using my Sonic HUD, then I can help in this regard -- I have a script for saving/pasting levels (which I only tested in Sonic 1 and hacks) which does all of the lag management, but it depends on files from the Sonic HUD for its UI. Edit: note: the UI for the resyncher conflicts with part of the UI of the HUD itself, so it is best not to use both at the same time.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
moozooh wrote:
To be fair, his question was only answered in this post. Indeed the two objects would be separated at 400 km/s while each one is moving at 200 km/s, regardless of observers (which are irrelevant as per his premise).
Not quite; specifying the observers is of paramount importance with anything having to do with speed in relativity -- except for the speed of light, all speeds are relative, and it is meaningless to speak of speed without specifying an observer (technically, this was true of Galilean relativity too, but the distinct concepts I mentioned in my other post are equal there, and the "invariant speed" is infinite). For an observer that sees both objects moving at 200 km/s toward each other, yes, they will be approaching each other at 400 km/s; for any other observer, the speed of approach will be different. Lets for simplicity take speeds as fractions of the speed of light; then, 200 km/s is equivalent to 66.67%. Using nothing but the relativistic formula for velocity composition:
 u'    (u - v) / c
--- = -------------
 c     1 - uv/c^2
(here in a form suited to using speeds as % of c) we can obtain the following table:
Observer	Object 1	Object 2	Approach	Relative
  speed 	  speed 	  speed 	  speed 	  speed
-99.90% 	 99.98% 	 99.50% 	  0.48% 	 92.31%
-90.00% 	 97.92% 	 58.33% 	 39.58% 	 92.31%
-80.00% 	 95.65% 	 28.57% 	 67.08% 	 92.31%
-70.00% 	 93.18% 	  6.25% 	 86.93% 	 92.31%
-66.67% 	 92.31% 	  0.00% 	 92.31% 	 92.31%	**
-60.00% 	 90.48% 	-11.11% 	101.59% 	 92.31%
-50.00% 	 87.50% 	-25.00% 	112.50% 	 92.31%
-40.00% 	 84.21% 	-36.36% 	120.57% 	 92.31%
-30.00% 	 80.56% 	-45.83% 	126.39% 	 92.31%
-20.00% 	 76.47% 	-53.85% 	130.32% 	 92.31%
-10.00% 	 71.88% 	-60.71% 	132.59% 	 92.31%
  0.00% 	 66.67% 	-66.67% 	133.33% 	 92.31%	*
 10.00% 	 60.71% 	-71.88% 	132.59% 	 92.31%
 20.00% 	 53.85% 	-76.47% 	130.32% 	 92.31%
 30.00% 	 45.83% 	-80.56% 	126.39% 	 92.31%
 40.00% 	 36.36% 	-84.21% 	120.57% 	 92.31%
 50.00% 	 25.00% 	-87.50% 	112.50% 	 92.31%
 60.00% 	 11.11% 	-90.48% 	101.59% 	 92.31%
 66.67% 	  0.00% 	-92.31% 	 92.31% 	 92.31%	**
 70.00% 	 -6.25% 	-93.18% 	 86.93% 	 92.31%
 80.00% 	-28.57% 	-95.65% 	 67.08% 	 92.31%
 90.00% 	-58.33% 	-97.92% 	 39.58% 	 92.31%
 99.90% 	-99.50% 	-99.98% 	  0.48% 	 92.31%
Here, I assume that an "observer speed" of 0% corresponds to an observer that sees both objects moving at a speed of 66.67% (line marked *); each line corresponds to another observer moving relative to that reference observer. "Object # speed" refers to the speed of object # relative to the current line's observer. This would fall in concept (2) in my other post. These two columns are always less than c. "Approach speed" is how fast object 1 approaches object 2 according to the current line's observer. This would fall in concept (1) in my other post. This is what I said is usually conflated with relative speed; it is "uncapped" except indirectly (because the object speeds are capped). As can be seen, it varies greatly, and depends heavily on the observer. "Relative speed" is the speed with which object 1 sees object 2 moving. This would fall in concept (2) in my other post. This column is what p4wn3r was referring to in his post; this was also what FODA was asking about. This one is constant -- but that is not because I made all entries equal, because I didn't! I put a formula in this column just as I did in all but the first column; the fact that they all come out equal is just a reflection of the self-consistency of the theory. See below for details on how I built the table. The lines marked with ** are the rest frames of either one of the moving objects; note that approach speed and relative speed become equal in these cases. As you can see, the objects can't be said to be approaching at a speed of 133.33% (400 km/s) except for one observer; and for sufficiently fast-moving observers (relative to line *), they are approaching much slower than that (the extreme lines, for example). This table also ignores motion on any direction other than the line between the two objects; were such motion considered, the table would be very, very different. As for the table: it was done in LibreOffice Calc, with the "observer speed" column in column 1 (first number at A1) using the following formulas in the other columns: B2: =(B$13-$A2)/(1-B$13*$A2) C2: =(C$13-$A2)/(1-C$13*$A2) D2: =B2-C2 E2: =(B2-C2)/(1-B2*C2) then drag/copy to all rows except row 13. Column A is for "initial values", as are B13 and C13; everything else used the formulas above. I configured all rows to show percentages, then hand-formatted the table after pasting it in a text editor.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
For what is worth, I would agree that the dances should only be used when they save time unless you tag the run with 'trades speed for entertainment'. I will miss the dances, yes, but I (at least) won't use it against the run.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
I will also add that there are two distinct concepts involved in your question that usually get mashed together because of our Galilean intuitions and cause needless confusion. They are:
  1. Rate of separation between two objects as seen by a third object;
  2. rate of separation between two objects as seen by one of them ("relative speed", "relative velocity").
Our intuition usually lumps those two concepts together -- in our day to day experience, there is no distinction between these two concepts, and (1) and (2) are numerically equal. But this happens only because the speeds involved in our daily lives are an insignificant fraction of the speed of light in vacuum; when speeds increase, this view leads to errors that grow very quickly. In relativity, concepts (1) and (2) are related, but not equal: the key aspects are time dilation and spatial contraction. Since time and space are different for each object, (1) and (2) can't be compared in the way our intuition wants to compare them. Both are rate of changes of position over time, but they use "different" rulers (spatial contraction) and "different" scales for time (time dilation). (2) can never exceed c; spatial contraction and time dilation see to that. This isn't a huge coincidence or conspiracy; merely a mathematical property of any spacetime where you have such a limiting speed. An analogy here is with a rotation -- rotations keep lengths intact, just as changing speeds keep speeds below c (to be honest, this isn't an analogy -- a change of speed, or "boost", is a rotation on a plane that contains the time axis). On the other hand, there is a much weaker limit to what value (1) can take -- it can range from -2c to +2c (with the extreme cases being light), since each object can't be moving away faster than c from the third object. Most people encountering relativity for the first time (which includes almost all physics students) have enormous difficulty because the difference between these concepts is not adequately explained -- mostly because they usually aren't explained at all.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
@FODA: In relativity (special or general), nothing massive can travel at or above the speed of light. In theory, if you have an imaginary mass, you can travel above the speed of light; these are the theoretical tachyons -- which physicists don't like much because they make spacetime unstable. Only things with zero mass (such as the photon) can travel at the speed of light. Moreover, relativity also prevents stuff from moving from one "category" to another without undergoing changes in mass and composition -- thus, no matter how strong your propulsion system is, or how long it can be kept on, you can't more at the speed of light unless you throw away all your mass, and you can't go above the speed of light unless you acquire some imaginary mass (and the converse if you started out as a tachyon). The main difference between special and general relativity is that in the latter, you can't move faster than light locally -- that is, you can't outrun that beam of light you just fired -- but you potentially can globally -- you send a beam of light one route, take a different route and still arrive at that distant star faster than your beam of light because the specifics of spacetime curvature gave you a shorter route. @Warp: at the speed of light, your proper time doesn't "tick". Hence, you can travel to any point in the Universe to any other point in zero time -- as long as these points are separated by a "null" or "lightlike" interval (basically, the spacetime distance between them comes out "zero"). Curved spacetime has the problem that the (spacetime) distance between any two points can have multiple values (and no, taking the minimum doesn't work -- even in the case of the flat spacetime of special relativity -- due to the hyperbolic nature of spacetime).
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
Miles wrote:
Disappointing to see how many people are saying the hack is bad just from watching this though. :(
Yeah... even despite the fact that I explicitly suggested people play it before watching the TAS.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
After Zeupar asked for it in IRC, I decided to post it here too: here is a dirty encode: Link to video
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
Mukki has it right -- it is better to wait for corroborating evidence. This is a single experiment -- which contradicts other experiments, and goes against a theory with over a century of high precision experiments riding on it, including the extremely sensitive GPS devices they used to compute position and distance of the labs involved. The most likely outcome of this is that an error will be found when the scientific community has more information and is analyzing the data. If it is not an error, scientists will still be happy, of course -- there will be new things to learn.
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
Here is the second WIP. I managed to improve many levels; random differences in lag forced me to do so. The game is not very hex friendly, as it has some random lag throughout -- resynching is a pain. I also cannot say how much faster the levels where because I haven't looked for the timer yet -- if there is one. The Casino Nightmare level took most of the rerecords -- I would estimate some 3000 of them -- and still it is the same as it was on the previous version. Despite managing to get to the ring room about 1 second faster, I simply could not reproduce the trick where I entered the ring without destroying the bumpers -- I was always 2 pixels too far up or down, and 2 pixels too far left or right, and I could not bump my way in; and this made the level ultimately slower. I freely admit that the first time I got it by luck -- and was smart enough to save it -- but I had no idea it was that much luck... The multiple choice level was fun -- the bottom route is faster to get to, as it is not blocked, and has less wait than the middle route; the climb after is not long enough to make it slower. There is the added bonus that this route is supposedly impossible :-) Here is the new encode: Link to video
Marzo Junior
marzojr
He/Him
Experienced Forum User, Published Author, Experienced player (751)
Joined: 9/29/2008
Posts: 964
Location: 🇫🇷 France
I didn't know that thing about holding the button, no. And yes, it is a WIP -- this is the first one, the first TAS play-through of the game I made. There is bound to have things I missed.
Marzo Junior