(Stuttering Craig voice) This is Sonic 3 Unlocked's 2017 Top 3 Christmas Corrections!
Number Three!
As part of my short series on Lock-on Technology, I pointed out a difference with Knuckles' climbing animation between Knuckles in Sonic 2 and Sonic & Knuckles: exclusively in the latter, whenever Knuckles stands still on a wall, he reverts back to the first frame of the climbing animation.
I chalked this up to a feature introduced in the S&K version of the Knuckles object, but later, an anonymous commenter performed their own analysis of the source code, which I present below. Turns out, it's not actually a feature, it's a bug:
I think the second behaviour quirk you mentioned in this post is the result of a bug. There's some code in S3K that isn't in KiS2, at loc_16E10 in the current S3K Git disasm. The equivalent label in KiS2's Git disasm is loc_315B04.Let's take a look at the code mentioned. Knuckles in Sonic 2 to the left, Sonic & Knuckles to the right. Changes in bold:
What I think this new code does is handle floor collision, because Knuckles still seems to move briefly after the player stops pressing the D-Pad. The issue is, this new code overwrites d1 with the distance Knuckles is from the floor. d1 is checked immediately afterwards, has Knuckles's frame ID added to it, and is then used to calculate which frame Knuckles should display.
d1 will always be a positive number, usually a large one depending on how far Knuckles is from the ground. This means, when Knuckles's frame ID is added to it, it goes well beyond the ceiling value of $BC, causing the game to reset it to $B7, making Knuckles display the first frame of his animation. Chances are the number could overflow, too, causing him to display his last frame instead.
Safe to say, editing the code to properly back up d1 causes it to behave like KiS2 instead.
loc_315B04: loc_16E10: move.b (Ctrl_1_logical).w,d0 andi.b #3,d0 bne.s loc_16E34 move.b $46(a0),d5 move.w $14(a0),d2 addi.w #9,d2 move.w $10(a0),d3 bsr.w sub_F828 tst.w d1 bmi.w loc_16D6E loc_16E34: tst.w d1 tst.w d1 beq.s loc_315B30 beq.s loc_16E60 subq.b #1,$1F(a0) subq.b #1,$25(a0) bpl.s loc_315B30 bpl.s loc_16E60 move.b #3,$1F(a0) move.b #3,$25(a0) add.b $1A(a0),d1 add.b $22(a0),d1 cmp.b #$B7,d1 cmpi.b #$B7,d1 bcc.s loc_315B22 bhs.s loc_16E52 move.b #$BC,d1 move.b #$BC,d1 loc_315B22: loc_16E52: cmp.b #$BC,d1 cmpi.b #$BC,d1 bls.s loc_315B2C bls.s loc_16E5C move.b #$B7,d1 move.b #$B7,d1 loc_315B2C: loc_16E5C: move.b d1,$1A(a0) move.b d1,$22(a0)In Sonic 2, when loc_315B04 is reached, the d1 register is set to 1, -1, or 0 depending on whether Knuckles is moving up, moving down, or standing still. Assuming neither branch to loc_315B30 is taken, Knuckles' current mapping frame is added to the value in d1, and then two bound checks are made before writing the resulting value back into Knuckles' mapping frame: if the value is less than $B7, d1 is set to $BC, and if it's greater than $BC, d1 is set to $B7.
The gist of it is: while Knuckles is climbing up a wall, his mapping frame gets progressively incremented, but when he's climbing down, it gets decremented instead. And if the mapping frame ever steps outside of the $B7-$BC range, it gets wrapped around to the other end of the range, in order to loop the animation.
In Sonic & Knuckles though, a call to sub_F828 was introduced, causing the FindFloor function to be called whenever the player is holding neither up nor down on the directional pad. The FindFloor function calculates an object's distance to the floor directly below it, and stores the result in register... d1.
The inevitable result follows: when the player lets go of the directional pad, sub_F828 is called and the value in d1 gets overwritten with the distance between the center of the Knuckles object and the floor. Knuckles' current mapping frame is then added to this value, which always produces a value greater than $BC. This triggers the bounds check, resetting Knuckles' mapping frame back to $B7, the first frame of the climbing animation.
In other words, the anonymous commenter's analysis is 100% correct. Good work!
Number Two!
On the subject of triggering slope glitch in Ice Cap Zone by having Tails break an ice block while Sonic is standing on it, Brainulator9 asked whether Tails could get slope glitch by instead breaking the block as Sonic. In Sonic 3 & Knuckles, this is impossible because player 2's status bits always get set, regardless of who breaks the blocks, and regardless of whether the Tails object is even present in the player 2 slot.
However, as Brainulator9 pointed out, the same isn't true of standalone Sonic 3, in which Sonic can indeed break Tails' gravity. Below is the relevant code: Sonic 3 to the left, Sonic & Knuckles to the right, once again changes in bold.
loc_58B3C: loc_8B384: move.b ($FFFFB020).w,$3A(a0) move.b ($FFFFB020).w,$3A(a0) move.b ($FFFFB06A).w,$3B(a0) move.b ($FFFFB06A).w,$3B(a0) moveq #$23,d1 moveq #$23,d1 moveq #$10,d2 moveq #$10,d2 moveq #$10,d3 moveq #$10,d3 move.w $10(a0),d4 move.w $10(a0),d4 jsr (SolidObjectFull).l jsr (SolidObjectFull).l bsr.w sub_58B62 bsr.w sub_8B3AA jmp (Sprite_OnScreen_Test).l jmp (Sprite_OnScreen_Test).l sub_58B62: sub_8B3AA: move.b $2A(a0),d0 move.b $2A(a0),d0 btst #3,d0 btst #3,d0 beq.s loc_58B78 beq.s loc_8B3C0 lea (Player_1).w,a1 lea (Player_1).w,a1 cmpi.b #2,$3A(a0) cmpi.b #2,$3A(a0) beq.s loc_58B8A beq.s loc_8B3D2 loc_58B78: loc_8B3C0: btst #4,d0 btst #4,d0 beq.s locret_58BD0 beq.s locret_8B430 lea (Player_1).w,a2 lea (Player_2).w,a1 cmpi.b #2,$3B(a0) cmpi.b #2,$3B(a0) bne.s locret_58BD0 bne.s locret_8B430 loc_58B8A: loc_8B3D2: bset #2,$2A(a1) bset #2,$2A(a1) move.b #$E,$1E(a1) move.b #$E,$1E(a1) move.b #7,$1F(a1) move.b #7,$1F(a1) move.b #2,$20(a1) move.b #2,$20(a1) move.w #-$300,$1A(a1) move.w #-$300,$1A(a1) bset #1,$2A(a1) bset #1,$2A(a1) bclr #3,$2A(a1) bclr #3,$2A(a1) move.b #2,5(a1) move.b #2,5(a1) btst #4,$2A(a0) beq.s loc_8B41A lea (Player_2).w,a1 bset #1,$2A(a1) bclr #3,$2A(a1) loc_8B41A: lea ChildObjDat_58C20(pc),a2 lea ChildObjDat_8B480(pc),a2 jsr CreateChild1_Normal(pc) jsr CreateChild1_Normal(pc) moveq #$6E,d0 moveq #$6E,d0 jsr (Play_Sound_2).l jsr (Play_Sound_2).l jsr (Go_Delete_Sprite).l jsr (Go_Delete_Sprite).l locret_58BD0: locret_8B430: rts rtsBoth versions of the code call the SolidObjectFull function, and then check bits 3 and 4 of the status bitfield along with the animation of the corresponding player, which is previously backed up to offsets $3A and $3B, in order to determine whether the player landed on the object whilst in their rolling animation.
Note how thoroughly botched the checks for player 2 are in Sonic 3, though: player 1's RAM address is loaded instead of player 2's, and it gets loaded to register a2 rather than register a1. The only reason this code works at all is because the SolidObjectFull function itself sets a1 to player 2's RAM address during the course of its execution, and then exits without overwriting the contents of the register with something else:
SolidObjectFull: lea (Player_1).w,a1 moveq #3,d6 movem.l d1-d4,-(sp) bsr.s sub_1BA2A movem.l (sp)+,d1-d4 lea (Player_2).w,a1 tst.b 4(a1) bpl.w locret_1BA6A addq.b #1,d6 sub_1BA2A: ...That isn't the problem in and of itself, however: the problem is that the code at loc_58B8A only runs for a single player, which leaves the other player hanging if they happened to also be standing on the ice block at the time. Rather than fix this properly, Sonic 3 & Knuckles simply forces player 2 to fall off the block either way, resulting in the strange, lopsided behavior where player 1 can get slope glitch but not player 2.
Number One!
Finally, regarding the Japanese characters in the slot machine bonus stage, another anonymous commenter points out that if you read them vertically, top to bottom, then left to right, they make up the first sixteen letters of the Iroha.
Now, what is the Iroha? It is an ancient Japanese poem, which has the unique characteristic of using every single kana character exactly once. (The title refers to the first three characters used in the poem.)
イロハニホヘト iro ha nihoheto |
チリヌルヲ chirinuru wo |
ワカヨタレソ wa ka yo tare so |
ツネナラム tsune naramu |
ウヰノオクヤマ uwi no okuyama |
ケフコエテ kefu koete |
アサキユメミシ asaki yume mishi |
ヱヒモセス wehi mo sesu |
Since each character only appears once, the Iroha serves as an alternative to the usual gojūon ordering, but both work equally well as placeholder graphics for a level's animated PLCs.
That's all I've got. Thank you all so much for the valuable feedback; I hope every single one of you has a terrific holiday season, and don't forget:
Merry Christmas!
ReplyDeleteHappy holidays!
ReplyDelete