don't click here

Sonic & Knuckles Collection C port

Discussion in 'Engineering & Reverse Engineering' started by BenoitRen, Jan 11, 2024.

  1. BenoitRen

    BenoitRen

    Tech Member
    433
    187
    43
    The port of the code related to starposts is done! There are some parts I'd like to refactor to cleaner, more C-like code, similar to what I did to patchg, but I want to try to always have the rough version committed first so it's part of the git history.

    Other cleanup was implementing Clownacy's suggestion for objects's "scratch RAM". More specifically, the unions version he recently described. It's not implemented everywhere yet, as I prefer to have a good understanding of an object type's scratch RAM use before adding it the list.

    Finally, I may have found another bug related to walking the sprite object array.

    The function Clear_SpriteRingMem is responsible for marking all objects as no longer needing to be spawned. To do that, it takes the address of the first dynamic sprite object, which is the fourth:
    Code (C):
    1. sprite_status* p_actwk = &actwk[3];
    There are 90 objects, so it initialises its counter to 89 (it stops when it turns negative).
    Code (C):
    1. short cnt = 89;
    Here's the loop it uses to walk the object array and mark them as no longer respawning:
    Code (C):
    1. do {
    2.   ++p_actwk;
    3.   if (p_actwk->r_ptr != 0) {
    4.     if (SPRITE_STATUS_USHORT(p_actwk, 72) != 0) {
    5.       CLEAR_BIT_7(flagwork[SPRITE_STATUS_USHORT(p_actwk, 72) - 1]);
    6.     }
    7.   }
    8.   --cnt;
    9. } while (cnt != -1);
    As you can see, the very first thing it does is move to the next object, meaning it never marks the very first dynamic sprite object, and will clear the first reserved sprite object (actwk[93]). As the same bug was found elsewhere (in Create_New_Sprite3), one might start to wonder if this was by design, or a happy coincidence.
     
    • Informative Informative x 1
    • List
  2. BenoitRen

    BenoitRen

    Tech Member
    433
    187
    43
    The port of the code related to rings is done! This includes rings waiting to be collected, rings that Sonic has lost, rings that are magnetically attracted to Sonic's lightning shield, and rings flying towards Sonic when he wins them from a slot machine.

    The game actually keeps track of how many rings the level still has, and how many rings Sonic has collected from the level. According to the wiki, these values are never used for anything. Cool to know: rings magnetically attracted by Sonic's lightning shield count as collected.
     
    • Informative Informative x 3
    • List
  3. saxman

    saxman

    Oldbie Tech Member
    Sounds like good progress so far.

    The rings remaining count may be a left-over from Sonic 2 (i.e. "perfect bonus"). Given the nature of Sonic 3 and how it's impossible for players to collect every single ring in those massive stages, I think it made sense to ditch that.
     
  4. Jeffery Mewtamer

    Jeffery Mewtamer

    Blind Bookworm Member
    1,890
    99
    28
    Huh, I thought the perfect bonus was still in S3/S&K/S3K even if the level design isn't really setup for it... Or does it only exist for Blue Spheres? And now I wonder if it's even possible in all S1/S2 zones...
     
  5. Kilo

    Kilo

    That inbetween sprite from S&K's title screen Tech Member
    347
    347
    63
    Canada
    S2 is pretty infamous for having rings in inaccessible spots due to being left in locations that were accessible in prototypes. S1 while it doesn't have out of bounds rings to my knowledge, can often force you to go down paths that split with no way to return and get the rings on alternate paths.
     
  6. BenoitRen

    BenoitRen

    Tech Member
    433
    187
    43
    Happy Eggman Day!

    A couple days ago I committed the code for the capsules found in Flying Battery Zone (obox04). Now I've committed the code for the end-of-act-2 animal capsules (opnbox).

    There are a few loose ends, however. In function opnbox_fly0 (loc_86642 in the disassembly), the id/address of the object in address register a1 is stored in the slot typically reserved for the child object. However, prior to that, a1 isn't filled in. opnbox_fly0 doesn't call any subroutines, and opnbox doesn't fill a1 in, either. As a result, I have no idea what a1 is supposed to be.

    Function set_friend_init is unfinished, because it addresses several unknown arrays. No doubt the logic for array indexing is wrong as a result. :(
     
  7. MainMemory

    MainMemory

    Kate the Wolf Tech Member
    4,743
    338
    63
    SonLVL
    Looks like nothing ever actually reads the value, maybe it's left over from a previous version of the code? I'd just leave it out personally.
     
  8. BenoitRen

    BenoitRen

    Tech Member
    433
    187
    43
    I guess that's possible. I'll remove it down the line when I go back to that file.

    In the meantime, I'd like gost08.c to compile. Now that I've ported the other objects it depends on, I've added their headers. But there's a reference to a Nemesis-compressed package that I'm not sure how to resolve, as I haven't ported Nemesis decompression yet.

    In function set_openbox_cg, there's a data structure called opnbox_cg_tbl. The first two bytes is the amount of PLCs (0, so one because zero-based), but I'm not sure about the rest of the data: 0xF8 0x2C 0x53h 0x00 0xC0 0xA6. The first four bytes could be an address to 00532cf8, but I can't confirm.

    The disassembly calls this data structure PLC_SOZGhostCapsule, and the contents are "plreq $536, ArtNem_EggCapsule".

    Finally, should I find this Nemesis-compressed data and its length, is there a better way of incorporating it into the code than a byte array?
     
  9. MainMemory

    MainMemory

    Kate the Wolf Tech Member
    4,743
    338
    63
    SonLVL
    The disassembly defines the plreq macro as follows (from sonic3k.macros.asm):
    Code (Text):
    1. ; macro for a pattern load request
    2. plreq macro toVRAMaddr,fromROMaddr
    3.     dc.l    fromROMaddr
    4.     dc.w    tiles_to_bytes(toVRAMaddr)
    5.     endm
    So yeah, the structure consists of a pointer to the source data, followed by the offset in VRAM that the data is decompressed to (tiles_to_bytes simply multiplies by $20, the size of a 4bpp 8x8 tile in bytes).

    You could include the art as a file, either through embedded resources or setting it up to load from a data folder next to the executable, but both would require additional setup for accessing the data via code. You could also use a pre-build tool to generate arrays from files in a data folder, rather than including any of them in the code directly.
     
    • Like Like x 1
    • Informative Informative x 1
    • List
  10. BenoitRen

    BenoitRen

    Tech Member
    433
    187
    43
    set_friend_init referring to unknown arrays was bugging me, so I looked at it again last night.

    Looking at the logic I could deduce the array member sizes. Then, looking at the data and remembering the context, I thought back to Sonic 3 Unlocked's post on the animals.

    The first unknown array contains the set of two animals per zone, selected based on the object's userflag1's second bit. The retrieved identifier is then used as an index for the second array, which contains the animal data. The four-byte offset is to skip some other data and directly retrieve the pointer to patbase.

    Curiously, multiple animals share a pattern with others. Pocky the rabbit and Pecky the pinguin share one. Cucky the chicken and Flicky the flicky share one. Sonic 3 Unlocked says that the retrieved identifier is used for the routine counter to determine if the animal should hop or fly. I think that the animals sharing a pattern do so because they share a movement type, which means that, besides hopping and flying, there's a third movement type (running?).

    As mentioned on Sonic 3 Unlocked, based on the values in the animals per zone set, there are 7 animals, with the fifth (Picky the pig) being unused. What's not mentioned, however, is that the animal data contains more entries. 12, in fact! Given that Sonic 3 was built on Sonic 2, and Sonic 2 contained 12 animals, this is likely a left-over.

    Finally, given that Sonic & Knuckles Collection also contains Sonic 3 alone and went out of its way to add code to Sonic & Knuckles's engine to replicate the authentic experience, I wonder if, when you want to play Sonic 3, it 'patches' the animals per zone set to restore Ricky the squirrel in Launch Base Zone (who is replaced by Flicky the flicky in Sonic & Knuckles and Sonic 3 & Knuckles).
     
    • Informative Informative x 1
    • List
  11. Brainulator

    Brainulator

    Regular garden-variety member Member
    The different types of patterns exist because the animals take different shapes: Cucky, Rocky, and Flicky use square movement art, Pocky and Pecky use art that's taller than it is wide, and Picky and Ricky use art that's wider than it is tall.
     
    • Informative Informative x 2
    • List
  12. BenoitRen

    BenoitRen

    Tech Member
    433
    187
    43
    The wiki refers to a mechanism that allows an object to remember it has been destroyed as "RSS". The value at 0x3C of the sprite status table stores the RAM address of a value to be modified. However, there doesn't seem to be something like an RSS table akin to a respawn table. What would this address be pointing to?
     
  13. BenoitRen

    BenoitRen

    Tech Member
    433
    187
    43
    I still don't know "RSS" means. The disassembly calls it "ros", and it has something to do with objects being "slotted".

    The link is set up in SetUp_ObjAttributesSlotted, so I decided to take a closer look. An offset is calculated from Slotted_object_bits to a "slot array", and it's the address of this array that is stored in the sprite status table. But what's the address of Slotted_object_bits?

    In the disassembly's constants file, there's a RAM map. Here's the relevant part:
    Code (ASM):
    1. _unkFA90           ds.w 1
    2. Camera_stored_max_X_pos       ds.w 1           ; the target camera maximum x-position
    3. Camera_stored_min_X_pos       ds.w 1           ; the target camera minimum x-position
    4. Camera_stored_min_Y_pos       ds.w 1           ; the target camera minimum y-position
    5. Camera_stored_max_Y_pos       ds.w 1           ; the target camera maximum y-position
    6. Slotted_object_bits       ds.w 1           ; bits to determine which slots are used for slotted objects
    7.            ds.b 6               ; unused
    8. _unkFAA2           ds.b 1
    Thanks to the labels of the surrounding unknown values, which mention the RAM address (offset 0xFF0000), the address of Slotted_object_bits can be calculated. Its address is 0xFA9A.

    Next, I decided to take a look at the offsets that are used. All of the objects used for SetUp_ObjAttributesSlotted have a ObjSlot_ prefix, making them easy to look up. Most have offset 0, with the other possible values being 2, 4, and 6.

    From this, it can be concluded that the data structure I'm looking for sits at 0xFA90-0xFAA1, which is an array of 4 words.
     
  14. MainMemory

    MainMemory

    Kate the Wolf Tech Member
    4,743
    338
    63
    SonLVL
    FYI, if you build the disassembly, it will produce a .lst file, that gives you the address and hex data associated with each line of code. That way you don't have to do any manual counting to find addresses.
     
    • Useful Useful x 2
    • Like Like x 1
    • List
  15. BenoitRen

    BenoitRen

    Tech Member
    433
    187
    43
    I did that research at work this morning because I didn't have anything to do, and GitHub can be argued to be work-related. :)

    Anyway, I get home to look at my existing code, because I suspected that I had already ported SetUp_ObjAttributesSlotted, and not only does the address of the array match, but I already have a name for what the disassembly calls "Slotted_object_bits": emy_wrt_flg, thanks to Sonic Jam. I just hadn't made the link with "RSS" or "ROS".

    The method that clears the object's entry from that array is called clr_wchg_bit. set_emy_wchg does "DPLC", so does wchg refer to an enemy's dynamically loaded patterns/tiles?
     
  16. Brainulator

    Brainulator

    Regular garden-variety member Member
    I think so, especially if it is taken to mean "write change".
     
  17. BenoitRen

    BenoitRen

    Tech Member
    433
    187
    43
    I've committed the code responsible for showing the score tallying at the end of an act. As I had no labels for this piece of code, I had to make up names myself, and didn't have any hints for any blocks. Luckily, the disassembly for this code is decently commented.

    The score tallying screen consists of 12 parts, and each part for bonus points has 8 sub-parts. But it doesn't create any objects for those sub-parts. No, it chooses to stuff them all into the same object!

    Starting position 0x10, the sprite_status object is used as follows:
    Code (C):
    1. struct {
    2.   short eldest_xposi;
    3.   unsigned char reserved[2];
    4.   short eldest_yposi;
    5.   unsigned short child_count;
    6.   sprite_status_child children[8];
    7.   unsigned char actfree[26];
    8. } family;
    The high words of xposi and yposi are retained, but the low word of yposi is used to store how many sub-objects (minus the first) it contains. Next, we have 8 slots for minimal sprite status objects. The definition is as follows:
    Code (C):
    1. struct sprite_status_child {
    2.   short xposi;
    3.   short yposi;
    4.   unsigned char reserved;
    5.   unsigned char patno;
    6. };
    As you can see, the fifth byte is reserved. This is done on purpose. By not touching the fifth byte of each sub-object, the data at that position can be used as normal. The reserved member of the second child has the same position as patno.

    Or they can be used to store other data, like how colino is used to store the part's position in the exit sequence (when all the parts slide left and right to exit the screen). In my opinion, it would have been cleaner if they would have used the actfree part of the object for that.

    To close things off, I saw in the code that you get a 100000 point bonus if you finish the act at exactly 9:59!
     
  18. BenoitRen

    BenoitRen

    Tech Member
    433
    187
    43
    I wanted to define the tile and pattern data arrays in a separate file so my compiler wouldn't complain anymore about those not being declared (which would make it easier to spot the actual errors). However, it turns out that static data structures can't contain a pointer, because that's not a constant.

    The reason I declared those static is because those data structures are supposed to be local data that can't be used anywhere else.

    Guess I'll have to live with those errors until I figure out a better way.
     
  19. Tiberious

    Tiberious

    Yeah, I'm furry. Got a problem? Oldbie
    778
    15
    18
    Yep. I encountered this once naturally in Carnival Night 2 myself. Was playing around too much in the stage, and started to run short on time. Had to quickly trash the boss and race to the capsule, making it last second and scoring that 100K.
     
  20. nineko

    nineko

    I am the Holy Cat Tech Member
    6,312
    489
    63
    italy
    It happened to me once, in Marble Garden (I don't remember which act), since I love that zone I went all around for a little too long.