Today is Christmas Day, a holiday which is typically celebrated by showering the people you love the most with copious amounts of gifts. Keeping with tradition, then, I thought this would be the perfect opportunity to celebrate three gifts that my readers have graciously offered me through the comments section of this blog.
(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.
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.
Let's take a look at the code mentioned. Knuckles in Sonic 2 to the left, Sonic & Knuckles to the right. Changes in
bold:
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 rts
Both 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: