Skip to content

Latest commit

 

History

History
653 lines (512 loc) · 25.4 KB

other_functions.md

File metadata and controls

653 lines (512 loc) · 25.4 KB

The rest of the functions in the actor

Now what?

At this point we have a choice to make. Either we could follow the main function flow and decompile func_80C10148, or take a look at Destroy, which for smaller actors can often be done straight after Init, since it usually just removes colliders and deallocates dynapoly.

Destroy

Destroy will be a dead end, but we might as well do it now. Usually we would regenerate the context first and apply it to mips2c as with Init, but if we look at the assembly...

glabel EnRecepgirl_Destroy
/* 0000FC 80C100CC AFA40000 */  sw          $a0, ($sp)
/* 000100 80C100D0 AFA50004 */  sw          $a1, 4($sp)
/* 000104 80C100D4 03E00008 */  jr          $ra
/* 000108 80C100D8 00000000 */   nop

It doesn't seem to do anything. Indeed, chucking it in mips2c,

$ ../mips_to_c/mips_to_c.py asm/non_matchings/overlays/ovl_En_Recepgirl/EnRecepgirl_Destroy.s
void EnRecepgirl_Destroy(s32 arg0, ? arg1) {

}

so it really does do nothing. It is worth staying on this briefly to understand what is is doing, though. Even with no context, mips2c knows it takes two arguments because it does two saves onto the stack: the calling convention the N64 uses requires the first four arguments be saved from the registers onto the stack, since the registers are expected to be cleared when a function call happens. It's done a bad job of guessing what they are, but that's to be expected: the assembly only tells us they're words. Thankfully we already know in this case, so we can just replace the GLOBAL_ASM by

void EnRecepgirl_Destroy(Actor* thisx, PlayState* play) {

}

and cross this function off.

func_80C10148

We don't really have a choice now, we have to look at this function. Remake the context (no need to change the function type this time), and run mips2c on the function's assembly file:

$ ../mips_to_c/mips_to_c.py asm/non_matchings/overlays/ovl_En_Recepgirl/func_80C10148.s data/ovl_En_Recepgirl/ovl_En_Recepgirl.data.s --context ctx.c
extern AnimationHeader D_0600AD98;
extern ? func_80C1019C;

void func_80C10148(EnRecepgirl *this) {
    SkelAnime *temp_a0;

    temp_a0 = &this->skelAnime;
    if (&D_06001384 == this->skelAnime.animation) {
        this = this;
        Animation_MorphToPlayOnce(temp_a0, &D_0600AD98, 5.0f);
    }
    this->actionFunc = &func_80C1019C;
}

This gives us some information immediately: D_0600AD98 is an AnimationHeader, and func_80C1019C is set as the action function. This means that we know its type, even though mips2c does not: looking in the header, we see the typedef is

typedef void (*EnRecepgirlActionFunc)(struct EnRecepgirl*, PlayState*);

and so we prototype func_80C1019C as

void func_80C1019C(EnRecepgirl* this, PlayState* play);

at the top (were it above the function we're currently working on, the prototype could eventually be replaced by the function definition itself, but since it isn't, it goes at the top with the others).

There are several rather odd things going on here:

  • temp_a0 is only used once. As such it's probably fake.
  • There's a weird this = this that does nothing
  • if (&D_06001384 == this->skelAnime.animation) is a bit of a funny way to write the condition: it seems more likely it would be the other way round.
  • Also, if we look up animation, we find it is an AnimationHeader*, so D_06001384 can be externed as AnimationHeader.
  • func_80C1019C is already a pointer, so the & is ineffectual. Our style is to not use & on function pointers.

If we tackle these, we end up with

void func_80C10148(EnRecepgirl* this);
void func_80C1019C(EnRecepgirl* this, PlayState* play);

[...]

extern AnimationHeader D_06001384;
extern AnimationHeader D_06009890;
extern UNK_TYPE D_0600A280;
extern AnimationHeader D_0600AD98;
extern FlexSkeletonHeader D_06011B60;

[...]

void func_80C10148(EnRecepgirl *this) {
    if (this->skelAnime.animation == &D_06001384) {
        Animation_MorphToPlayOnce(&this->skelAnime, &D_0600AD98, 5.0f);
    }
    this->actionFunc = func_80C1019C;
}

This is a common type of function called a setup (action) function. It runs once and prepares the ground for its corresponding actionfunction to run, whereas the actionfunction is usually run every frame by Update (but more on that later). Running make, we get OK again.

Again we have only one way to go

func_80C1019C

Remake the context and run mips2c on this function's assembly file. We get

? func_80C10290(EnRecepgirl *); // extern

void func_80C1019C(EnRecepgirl* this, PlayState* play) {
    SkelAnime *sp24;
    SkelAnime *temp_a0;

    temp_a0 = &this->skelAnime;
    sp24 = temp_a0;
    if (SkelAnime_Update(temp_a0) != 0) {
        if (&D_0600A280 == this->skelAnime.animation) {
            Animation_MorphToPlayOnce(temp_a0, &D_0600AD98, 5.0f);
        } else {
            Animation_ChangeTransitionRepeat(temp_a0, &D_06009890, -4.0f);
        }
    }
    if (Actor_TalkOfferAccepted((Actor *) this, play) != 0) {
        func_80C10290(this);
        return;
    }
    if (Actor_IsFacingPlayer((Actor *) this, 0x2000) != 0) {
        Actor_OfferTalk((Actor *) this, play, 60.0f);
        if (Player_GetMask(play) == 2) {
            this->actor.textId = 0x2367;
            return;
        }
        if (Flags_GetSwitch(play, (s32) this->actor.params) != 0) {
            this->actor.textId = 0x2ADC;
            return;
        }
        this->actor.textId = 0x2AD9;
        // Duplicate return node #12. Try simplifying control flow for better match
    }
}

This is a bit juicier! We can do some preliminary cleanup, then worry about the control flow.

  • sp24 does nothing, so is almost certainly fake.
  • temp_a0 is used in 3 different places, but they're all right next to one another and are unlikely to be required since there's no nontrivial calculation or anything happening. Let's remove it too and see what happens.
  • We've got another reversed comparison, &D_0600A280 == this->skelAnime.animation.
  • D_0600A280 is an AnimationHeader.
  • (Actor *) this should be replaced by &this->actor.
  • `Flags_GetSwitch is a boolean and we don't need to cast the argument, as we have discussed before. (We don't know about the other functions in the conditions, so leave them for now.)
  • Prototype func_80C10290: it is reasonable to guess it's another setup function, so void func_80C10290(EnRecepgirl* this);.

Changing all these, we end up with

void func_80C10148(EnRecepgirl* this);
void func_80C1019C(EnRecepgirl* this, PlayState* play);
void func_80C10290(EnRecepgirl* this);

[...]

extern AnimationHeader D_06001384;
extern AnimationHeader D_06009890;
extern AnimationHeader D_0600A280;
extern AnimationHeader D_0600AD98;
extern FlexSkeletonHeader D_06011B60;

[...]

void func_80C1019C(EnRecepgirl* this, PlayState* play) {
    if (SkelAnime_Update(&this->skelAnime) != 0) {
        if (&D_0600A280 == this->skelAnime.animation) {
            Animation_MorphToPlayOnce(&this->skelAnime, &D_0600AD98, 5.0f);
        } else {
            Animation_ChangeTransitionRepeat(&this->skelAnime, &D_06009890, -4.0f);
        }
    }
    if (Actor_TalkOfferAccepted(&this->actor, &play->state) != 0) {
        func_80C10290(this);
        return;
    }
    if (Actor_IsFacingPlayer(&this->actor, 0x2000) != 0) {
        Actor_OfferTalk(&this->actor, play, 60.0f);
        if (Player_GetMask(play) == 2) {
            this->actor.textId = 0x2367;
            return;
        }
        if (Flags_GetSwitch(play, this->actor.params)) {
            this->actor.textId = 0x2ADC;
            return;
        }
        this->actor.textId = 0x2AD9;
        // Duplicate return node #12. Try simplifying control flow for better match
    }
}

If we look with diff.py, we find this matches. But we can replace some of the returns by elses: generally, we use elses unless

  • After an Actor_Kill
  • Sometimes after setting an actionfunction
  • There's no way to avoid an early return

Here, it's debatable whether to keep the first, since func_80C10290 is likely a setup function. The latter two should be changed to elses, though. For now, let's replace all of them. This leaves us with

void func_80C1019C(EnRecepgirl* this, PlayState* play) {
    if (SkelAnime_Update(&this->skelAnime) != 0) {
        if (this->skelAnime.animation == &D_0600A280) {
            Animation_MorphToPlayOnce(&this->skelAnime, &D_0600AD98, 5.0f);
        } else {
            Animation_ChangeTransitionRepeat(&this->skelAnime, &D_06009890, -4.0f);
        }
    }

    if (Actor_TalkOfferAccepted(&this->actor, &play->state) != 0) {
        func_80C10290(this);
    } else if (Actor_IsFacingPlayer(&this->actor, 0x2000)) {
        Actor_OfferTalk(&this->actor, play, 60.0f);
        if (Player_GetMask(play) == 2) {
            this->actor.textId = 0x2367;
        } else if (Flags_GetSwitch(play, this->actor.params)) {
            this->actor.textId = 0x2ADC;
        } else {
            this->actor.textId = 0x2AD9;
        }
    }
}

which still matches. Lastly, we have an enum for the output of Player_GetMask and other mask-related things: in z64player.h we find

typedef enum {
    /* 0x00 */  PLAYER_MASK_NONE,
    /* 0x01 */  PLAYER_MASK_MASK_OF_TRUTH,
    /* 0x02 */  PLAYER_MASK_KAFEIS_MASK,
[...]
    /* 0x19 */  PLAYER_MASK_MAX
} PlayerMask;

and so we can write the last if as Player_GetMask(play) == PLAYER_MASK_KAFEIS_MASK.

Again, we have no choice in what to do next.

func_80C10290

Remaking the context and running mips2c gives

void func_80C102D4(EnRecepgirl*, PlayState*); // extern

void func_80C10290(EnRecepgirl *this) {
    Animation_MorphToPlayOnce(&this->skelAnime, &D_0600A280, -4.0f);
    this->actionFunc = func_80C102D4;
}

so all we have to do is add the function prototype for the newest action function. Not surprisingly, this matches without changing anything.

func_80C102D4

Large code block, click to show
extern AnimationHeader D_06000968;

void func_80C102D4(EnRecepgirl* this, PlayState* play) {
    SkelAnime *sp20;
    AnimationHeader *temp_v0;
    SkelAnime *temp_a0;
    u16 temp_v0_3;
    u16 temp_v0_4;
    u8 temp_v0_2;

    temp_a0 = &this->skelAnime;
    sp20 = temp_a0;
    if (SkelAnime_Update(temp_a0) != 0) {
        temp_v0 = this->skelAnime.animation;
        if (&D_0600A280 == temp_v0) {
            Animation_ChangeDefaultRepeat(sp20, &D_06001384);
        } else if (&D_0600AD98 == temp_v0) {
            if (this->actor.textId == 0x2ADA) {
                Animation_MorphToPlayOnce(sp20, &D_06000968, 10.0f);
            } else {
                Animation_ChangeTransitionRepeat(sp20, &D_06009890, 10.0f);
            }
        } else if (this->actor.textId == 0x2ADA) {
            Animation_ChangeTransitionRepeat(sp20, &D_06009890, 10.0f);
        } else {
            Animation_MorphToPlayOnce(sp20, &D_0600A280, -4.0f);
        }
    }
    temp_v0_2 = Message_GetState(&play->msgCtx);
    if (temp_v0_2 == 2) {
        this->actor.textId = 0x2ADC;
        func_80C10148(this);
        return;
    }
    if (((temp_v0_2 & 0xFF) == 5) && (Message_ShouldAdvance(play) != 0)) {
        temp_v0_3 = this->actor.textId;
        if (temp_v0_3 == 0x2AD9) {
            Flags_SetSwitch(play, (s32) this->actor.params);
            Animation_MorphToPlayOnce(sp20, &D_0600AD98, 10.0f);
            if ((*(&gSaveContext + 0xF37) & 0x80) != 0) {
                this->actor.textId = 0x2ADF;
            } else {
                this->actor.textId = 0x2ADA;
            }
        } else if (temp_v0_3 == 0x2ADC) {
            Animation_MorphToPlayOnce(sp20, &D_0600AD98, 10.0f);
            this->actor.textId = 0x2ADD;
        } else {
            Animation_MorphToPlayOnce(sp20, &D_06000968, 10.0f);
            temp_v0_4 = this->actor.textId;
            if (temp_v0_4 == 0x2ADD) {
                this->actor.textId = 0x2ADE;
            } else if (temp_v0_4 == 0x2ADA) {
                this->actor.textId = 0x2ADB;
            } else {
                this->actor.textId = 0x2AE0;
            }
        }
        Message_ContinueTextbox(play, this->actor.textId);
    }
}

Well, this is a big one! We get one more extern, for D_06000968. A lot of the temps used in the conditionals look fake, with the exception of temp_v0_2: because the function is only called once but the temp is used twice, the temp must be real. Removing the others and switching the animation conditionals,

void func_80C102D4(EnRecepgirl* this, PlayState* play) {
    u8 temp_v0_2;

    if (SkelAnime_Update(&this->skelAnime) != 0) {
        if (this->skelAnime.animation == &D_0600A280) {
            Animation_ChangeDefaultRepeat(&this->skelAnime, &D_06001384);
        } else if (this->skelAnime.animation == &D_0600AD98) {
            if (this->actor.textId == 0x2ADA) {
                Animation_MorphToPlayOnce(&this->skelAnime, &D_06000968, 10.0f);
            } else {
                Animation_ChangeTransitionRepeat(&this->skelAnime, &D_06009890, 10.0f);
            }
        } else if (this->actor.textId == 0x2ADA) {
            Animation_ChangeTransitionRepeat(&this->skelAnime, &D_06009890, 10.0f);
        } else {
            Animation_MorphToPlayOnce(&this->skelAnime, &D_0600A280, -4.0f);
        }
    }

    temp_v0_2 = Message_GetState(&play->msgCtx);
    if (temp_v0_2 == 2) {
        this->actor.textId = 0x2ADC;
        func_80C10148(this);
        return;
    }

    if (((temp_v0_2 & 0xFF) == 5) && (Message_ShouldAdvance(play) != 0)) {
        if (this->actor.textId == 0x2AD9) {
            Flags_SetSwitch(play, this->actor.params);
            Animation_MorphToPlayOnce(&this->skelAnime, &D_0600AD98, 10.0f);
            if ((*(&gSaveContext + 0xF37) & 0x80) != 0) {
                this->actor.textId = 0x2ADF;
            } else {
                this->actor.textId = 0x2ADA;
            }
        } else if (this->actor.textId == 0x2ADC) {
            Animation_MorphToPlayOnce(&this->skelAnime, &D_0600AD98, 10.0f);
            this->actor.textId = 0x2ADD;
        } else {
            Animation_MorphToPlayOnce(&this->skelAnime, &D_06000968, 10.0f);
            if (this->actor.textId == 0x2ADD) {
                this->actor.textId = 0x2ADE;
            } else if (this->actor.textId == 0x2ADA) {
                this->actor.textId = 0x2ADB;
            } else {
                this->actor.textId = 0x2AE0;
            }
        }
        Message_ContinueTextbox(play, this->actor.textId);
    }
}

There remains one thing we need to fix before trying to compile it, namely *(&gSaveContext + 0xF37) & 0x80. This is really a funny way of writing an array access, because mips2c will get confused about arrays in structs. Opening up z64save.h, we find in the SaveContext struct that

    /* 0x0EF8 */ u8 weekEventReg[100];       // "week_event_reg"
    /* 0x0F5C */ u32 regionsVisited;         // "area_arrival"

so it's somewhere in weekEventReg. 0xF37 - 0xEF8 = 0x3F = 63, and it's a byte array, so the access is actually gSaveContext.save.saveInfo.weekEventReg[63] & 0x80. Now it will compile. We also don't use != 0 for flag comparisons: just if (gSaveContext.save.saveInfo.weekEventReg[63] & 0x80) will do.

Running ./diff.py -mwo3 func_80C102D4 and scrolling down, we discover that this doesn't match!

First run of diff.py on func_80C102D4

The yellow shows registers that don't match, the different colours on the registers help you to estimate where the problems are. Usually it's best to start at the top and work down if possible: any regalloc problems at the top tend to propagate most of the way down. In our case, the first problem is

3f0:    andi    t0,v0,0xff                        r  153 3f0:    andi    t1,v0,0xff

somehow we skipped over t0. Where is this in the code? The 153 in the middle is the line number in the C file (the 3f0s are the offsets into the assembly file), we have --source if you want to see the code explicitly, or you can do it the old-fashioned way, and work it out from nearby function calls. In this case, func_80C10148 is run straight after, and the only place that is called is

    temp_v0_2 = Message_GetState(&play->msgCtx);
    if (temp_v0_2 == 2) {
        this->actor.textId = 0x2ADC;
        func_80C10148(this);
        return;
    }

    if (((temp_v0_2 & 0xFF) == 5) && (Message_ShouldAdvance(play) != 0)) {

If you look at the conditionals and the declaration of temp_v0_2, you may notice something odd: temp_v0_2 is a u8. Therefore the & 0xFF does nothing! It's surprisingly common for this to happen, be it leaving out a & 0xFF or adding an extraneous one. If we remove it, we get a match:

func_80C102D4 now matching

Notice that indeed the subsequent regalloc, which might have looked like a bigger problem than the initial part, was also fixed: skipping a register in one place will throw the registers off below too.

And now we've run out of functions. Time for Update.

Update

Update runs every frame and usually is responsible for the actor's common logic updates: for example, updating timers, blinking, updating collision, running the actionFunc, and so on, either directly or through other functions it calls. A lot of subsidiary functions that are not common to every state (e.g. updating position, or the text when talking, etc.) are carried out by one of the action functions we have already decomped.

Remake the context and run mips2c:

? func_80C100DC(EnRecepgirl *); // extern

void EnRecepgirl_Update(Actor* thisx, PlayState* play) {
    EnRecepgirl* this = (EnRecepgirl *) thisx;
    ? sp30;

    this->actionFunc(this, play);
    Actor_TrackPlayer(play, (Actor *) this, this + 0x2AE, (Vec3s *) &sp30, (bitwise Vec3f) this->actor.focus.pos.x, this->actor.focus.pos.y, this->actor.focus.pos.z);
    func_80C100DC(this);
}

If we search for func_80C100DC, we find that this is the only time it is used. Hence we can be almost certain that its prototype is void func_80C100DC(EnRecepgirl* this);. This function occurs above Update, so you can put the prototype next to the GLOBAL_ASM and remove it when we decompile that function.

Change the function and the prototype back to Actor* thisx, and add the casting temp:

void func_80C100DC(EnRecepgirl *);
#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Recepgirl/func_80C100DC.s")

[...]

void EnRecepgirl_Update(Actor* thisx, PlayState* play) {
    EnRecepgirl* this = THIS;
    ? sp30;

    this->actionFunc(this, play);
    Actor_TrackPlayer(play, &this->actor, this + 0x2AE, (Vec3s *) &sp30, (bitwise Vec3f) this->actor.focus.pos.x, this->actor.focus.pos.y, this->actor.focus.pos.z);
    func_80C100DC(this);
}

Now, our problem is Actor_TrackPlayer. The arguments all look terrible! Indeed, if we look at the actual function in src/code/code_800E8EA0.c (found by searching), we find that it should be

s32 Actor_TrackPlayer(PlayState* play, Actor* actor, Vec3s* headRot, Vec3s* torsoRot, Vec3f focusPos)

So mips2c has made a bit of a mess here:

  • the third argument should be a Vec3s. Hence this + 0x2AE is a Vec3s*, and so this->unk_2AE is a Vec3s
  • &sp30 is a Vec3s*, so sp30 is a Vec3s (it's clearly not used for anything, just used to "dump" a side-effect of the function)
  • The last argument is supposed to be an actual Vec3f

Fixing all of this, we end up with

void EnRecepgirl_Update(EnRecepgirl* this, PlayState* play) {
    EnRecepgirl* this = THIS;
    Vec3s sp30;

    this->actionFunc(this, play);
    Actor_TrackPlayer(play, &this->actor, &this->unk_2AE, &sp30, this->actor.focus.pos);
    func_80C100DC(this);
}

and can fill in the top end of the struct:

typedef struct EnRecepgirl {
    /* 0x0000 */ Actor actor;
    /* 0x0144 */ SkelAnime skelAnime;
    /* 0x0188 */ Vec3s jointTable[24];
    /* 0x0218 */ Vec3s morphTable[24];
    /* 0x02A8 */ EnRecepgirlActionFunc actionFunc;
    /* 0x02AC */ u8 unk_2AC;
    /* 0x02AD */ char unk_2AD[0x1];
    /* 0x02AE */ Vec3s unk_2AE;
} EnRecepgirl; // size = 0x2B4

It's entirely possible that unk_2AD is not real, and is just padding: see Types, structs, and padding for the details. We'll find out once we've finished all the functions. If we look at the diff, we find that one line is different:

EnRecepgirl_Update's stack difference

So sp30 is in the wrong place: it's 4 too high on the stack in ours. This is because the main four functions do not actually take PlayState: they really take Gamestate and recast it with a temp, just like EnRecepgirl* this = THIS;. We haven't implemented this in the repo yet, though, so for now, it suffices to put a pad on the stack where it would go instead: experience has shown when it matters, it goes above the actor recast, so we end up with

void EnRecepgirl_Update(Actor* thisx, PlayState* play) {
    s32 pad;
    EnRecepgirl* this = THIS;
    Vec3s sp30;

    this->actionFunc(this, play);
    Actor_TrackPlayer(play, &this->actor, &this->unk_2AE, &sp30, this->actor.focus.pos);
    func_80C100DC(this);
}

and this now matches.

N.B. sometimes using an actual PlayState* play temp is required for matching: add it to your bag o' matching memes.

*Some remarks about the function stack

(Feel free to skip this if you'd rather finish the actor first.)

The (function) stack is used to store variables. It has rather more space and is somewhat less volatile than registers (it still can't be used outside a function, except by a called function accessing its arguments). The stack a function sets up for itself to use is called its stack frame or function frame (or just function stack or the stack, although strictly speaking the frame itself is not a stack, since not just the top variable is accessed), and the function frames themselves form an (genuine) stack called the call stack. In MIPS this stack grows downwards, and its size is always a multiple of 0x8 (in case you want to put a 64-bit value on it, although almost no N64 game functions do this). The compiler uses the stack in a single function frame in the following way:

| user-defined variables | | compiler-defined varibles | | saved registers | | argument registers/stack |

where sp is at the very bottom of this table, and the function that called the current function would have its frame above this one. We have seen a couple of aspects of this stack behaviour already: saving the function arguments onto it in EnRecepgirl_Destroy, and here, requiring an extre user stack variable to be the correct size.

Anyway, back to EnRecepgirl. 4 functions to go...

func_80C100DC

This is the final non-draw function. You know what to do now: remake the context and run mips2c:

void func_80C100DC(EnRecepgirl *this) {
    u8 temp_t6;
    u8 temp_v0;

    temp_v0 = this->unk_2AC;
    temp_t6 = temp_v0 + 1;
    if (temp_v0 != 0) {
        this->unk_2AC = temp_t6;
        if ((temp_t6 & 0xFF) == 4) {
            this->unk_2AC = 0;
            return;
        }
        // Duplicate return node #5. Try simplifying control flow for better match
        return;
    }
    if (Rand_ZeroOne() < 0.02f) {
        this->unk_2AC += 1;
    }
}

Well, hmm. It's pretty hard to tell what's going on here. Finally it's time to really use diff.py. this function is a bit more typical of what to expect: this actor has been very easy so far!

func_80C100DC, first diff

Well, it's still pretty close. But the registers are all wrong. Firstly, temp_t6 is already u8, so the & 0xFF is again ineffective, so let's try removing it...

func_80C100DC, second diff

It's not obvious that did much: it even looks a bit worse.

    temp_v0 = this->unk_2AC;
    temp_t6 = temp_v0 + 1;
    if (temp_v0 != 0) {
        this->unk_2AC = temp_t6;

may remind you of that loop we decompiled, where mips2c unnecessarily made two temps. Let's walk through what this does.

  • First, it saves the value of this->unk_2AC into v0
  • Then, it adds one to it and stores it in t6.
  • It checks if the first saved value is zero
  • If it is, it sets this->unk_2AC to the incremented value and carries on.

Well, if we allow ourselves to bend the order of operations a little, there's a much simpler way to write this with no temps, namely

    if (this->unk_2AC != 0) {
        this->unk_2AC++;

So let's try removing both temps:

void func_80C100DC(EnRecepgirl *this) {
    if (this->unk_2AC != 0) {
        this->unk_2AC++;
        if (this->unk_2AC == 4) {
            this->unk_2AC = 0;
            return;
        }
        return;
    }
    if (Rand_ZeroOne() < 0.02f) {
        this->unk_2AC++;
    }
}

func_80C100DC, matching

There we go.

Even though this matches, it is not quite according to our style: remember what was said earlier about early returns. Here, both of them can be removed and replaced by a single else without affecting matching:

void func_80C100DC(EnRecepgirl *this) {
    if (this->unk_2AC != 0) {
        this->unk_2AC++;
        if (this->unk_2AC == 4) {
            this->unk_2AC = 0;
        }
    } else if (Rand_ZeroOne() < 0.02f) {
        this->unk_2AC++;
    }
}

and this is how we prefer it to be written.

With that, the last remaining function is EnJj_Draw. Draw functions have an extra layer of macroing that is required, so we shall cover them separately.

Next: Draw functions