diff options
| author | engineer124 <47598039+engineer124@users.noreply.github.com> | 2025-06-04 11:59:25 +1000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-03 18:59:25 -0700 |
| commit | d31ceacfdf468456d1052ca8af981f06040e0ac4 (patch) | |
| tree | 77ace096ed16ddcb9e88bfd264f722e2fadf1833 /src/code | |
| parent | ace4043d1a244185423837cb85829024f5e79a2c (diff) | |
Document Player's Face and z_actor FaceChange functions (#1777)
* player face docs
* more docs
* cleanup
* toto
* more comments
* fix bss, names
* better comment
* PR, fix comment
* fix bss
* sEyeTextures comment
* FaceChange_UpdateBlinkingNonHuman
---------
Co-authored-by: Anghelo Carvajal <angheloalf95@gmail.com>
Co-authored-by: Derek Hensley <hensley.derek58@gmail.com>
Diffstat (limited to 'src/code')
| -rw-r--r-- | src/code/game.c | 2 | ||||
| -rw-r--r-- | src/code/z_actor.c | 99 | ||||
| -rw-r--r-- | src/code/z_player_lib.c | 214 |
3 files changed, 241 insertions, 74 deletions
diff --git a/src/code/game.c b/src/code/game.c index 290f474a9..00539001a 100644 --- a/src/code/game.c +++ b/src/code/game.c @@ -16,6 +16,8 @@ #include "z64vimode.h" #include "z64vis.h" +#pragma increment_block_number "n64-us:128" + s32 gFramerateDivisor = 1; f32 gFramerateDivisorF = 1.0f; f32 gFramerateDivisorHalf = 1.0f / 2.0f; diff --git a/src/code/z_actor.c b/src/code/z_actor.c index e54fc18e7..954e786ed 100644 --- a/src/code/z_actor.c +++ b/src/code/z_actor.c @@ -3831,51 +3831,90 @@ void Enemy_StartFinishingBlow(PlayState* play, Actor* actor) { SoundSource_PlaySfxAtFixedWorldPos(play, &actor->world.pos, 20, NA_SE_EN_LAST_DAMAGE); } -// blinking routine -s16 func_800BBAC0(BlinkInfo* info, s16 arg1, s16 arg2, s16 arg3) { - if (DECR(info->blinkTimer) == 0) { - info->blinkTimer = Rand_S16Offset(arg1, arg2); - } - - if (info->blinkTimer - arg3 > 0) { - info->eyeTexIndex = 0; - } else if ((info->blinkTimer - arg3 >= -1) || (info->blinkTimer < 2)) { - info->eyeTexIndex = 1; +/** + * Updates `FaceChange` data for a blinking pattern. + * This system expects that the actor using the system has defined 3 faces in this exact order: + * "eyes open", "eyes half open", "eyes closed". + * + * @param faceChange pointer to an actor's faceChange data + * @param blinkIntervalBase The base number of frames between blinks + * @param blinkIntervalRandRange The range for a random number of frames that can be added to `blinkIntervalBase` + * @param blinkDuration The number of frames it takes for a single blink to occur + */ +s16 FaceChange_UpdateBlinking(FaceChange* faceChange, s16 blinkIntervalBase, s16 blinkIntervalRandRange, + s16 blinkDuration) { + if (DECR(faceChange->timer) == 0) { + faceChange->timer = Rand_S16Offset(blinkIntervalBase, blinkIntervalRandRange); + } + + if (faceChange->timer - blinkDuration > 0) { + // `timer - duration` is positive so this is the default state: "eyes open" face + faceChange->face = 0; + } else if ((faceChange->timer - blinkDuration >= -1) || (faceChange->timer < 2)) { + // This condition aims to catch both cases where the "eyes half open" face is needed. + // Note that the comparison assumes the duration of the "eyes half open" phase is 2 frames, irrespective of the + // value of `blinkDuration`. The duration for the "eyes closed" phase is `blinkDuration - 4`. + // For Player's use case `blinkDuration` is 6, so the "eyes closed" phase happens to have + // the same duration as each "eyes half open" phase. + faceChange->face = 1; } else { - info->eyeTexIndex = 2; + // If both conditions above fail, the only possibility left is the "eyes closed" face + faceChange->face = 2; } - return info->eyeTexIndex; + return faceChange->face; } -// blinking routine -s16 func_800BBB74(BlinkInfo* info, s16 arg1, s16 arg2, s16 arg3) { - if (DECR(info->blinkTimer) == 0) { - info->blinkTimer = Rand_S16Offset(arg1, arg2); +/** + * Updates `FaceChange` data for a blinking pattern. + * This system expects that the actor using the system has defined 3 faces in this exact order: + * "eyes open", "eyes half open", "eyes closed". + * + * @param faceChange pointer to an actor's faceChange data + * @param blinkIntervalBase The base number of frames between blinks + * @param blinkIntervalRandRange The range for a random number of frames that can be added to `blinkIntervalBase` + * @param blinkDuration The number of frames it takes for a single blink to occur + */ +s16 FaceChange_UpdateBlinkingNonHuman(FaceChange* faceChange, s16 blinkIntervalBase, s16 blinkIntervalRandRange, + s16 blinkDuration) { + if (DECR(faceChange->timer) == 0) { + faceChange->timer = Rand_S16Offset(blinkIntervalBase, blinkIntervalRandRange); } - if (info->blinkTimer - arg3 > 0) { - info->eyeTexIndex = 0; - } else if (info->blinkTimer - arg3 == 0) { - info->eyeTexIndex = 1; + if (faceChange->timer - blinkDuration > 0) { + // `timer - duration` is positive so this is the default state: "eyes open" face + faceChange->face = 0; + } else if (faceChange->timer - blinkDuration == 0) { + faceChange->face = 1; } else { - info->eyeTexIndex = 2; + // If both conditions above fail, the only possibility left is the "eyes closed" face + faceChange->face = 2; } - return info->eyeTexIndex; + return faceChange->face; } -// unused blinking routine -s16 func_800BBC20(BlinkInfo* info, s16 arg1, s16 arg2, s16 arg3) { - if (DECR(info->blinkTimer) == 0) { - info->blinkTimer = Rand_S16Offset(arg1, arg2); - info->eyeTexIndex++; - if ((info->eyeTexIndex % 3) == 0) { - info->eyeTexIndex = (s32)(Rand_ZeroOne() * arg3) * 3; +/** + * Updates `FaceChange` data for randomly selected face sets. + * Each set contains 3 faces. After the timer runs out, the next face in the set is used. + * After the third face in a set is used, a new face set is randomly chosen. + * + * @param faceChange pointer to an actor's faceChange data + * @param changeTimerBase The base number of frames between each face change + * @param changeTimerRandRange The range for a random number of frames that can be added to `changeTimerBase` + * @param faceSetRange The max number of face sets that will be chosen from + */ +s16 FaceChange_UpdateRandomSet(FaceChange* faceChange, s16 changeTimerBase, s16 changeTimerRandRange, + s16 faceSetRange) { + if (DECR(faceChange->timer) == 0) { + faceChange->timer = Rand_S16Offset(changeTimerBase, changeTimerRandRange); + faceChange->face++; + if ((faceChange->face % 3) == 0) { + faceChange->face = (s32)(Rand_ZeroOne() * faceSetRange) * 3; } } - return info->eyeTexIndex; + return faceChange->face; } void Actor_SpawnBodyParts(Actor* actor, PlayState* play, s32 partParams, Gfx** dList) { diff --git a/src/code/z_player_lib.c b/src/code/z_player_lib.c index 483d371c9..85d224010 100644 --- a/src/code/z_player_lib.c +++ b/src/code/z_player_lib.c @@ -1837,46 +1837,156 @@ Gfx gCullFrontDList[] = { gsSPEndDisplayList(), }; -TexturePtr sPlayerEyesTextures[PLAYER_EYES_MAX] = { - gLinkHumanEyesOpenTex, // PLAYER_EYES_OPEN - gLinkHumanEyesHalfTex, // PLAYER_EYES_HALF - gLinkHumanEyesClosedTex, // PLAYER_EYES_CLOSED - gLinkHumanEyesRollRightTex, // PLAYER_EYES_ROLL_RIGHT - gLinkHumanEyesRollLeftTex, // PLAYER_EYES_ROLL_LEFT - gLinkHumanEyesRollUpTex, // PLAYER_EYES_ROLL_UP - gLinkHumanEyesRollDownTex, // PLAYER_EYES_ROLL_DOWN - object_link_child_Tex_003800, // PLAYER_EYES_7 -}; - -TexturePtr sPlayerMouthTextures[PLAYER_MOUTH_MAX] = { +/** + * Link's eyes and mouth textures are placed at the exact same place in all player form's respective object files. + * This allows the array to only contain the symbols for one file and have it apply to all of them. This is a problem + * for shiftability, and changes will need to be made in the code to account for this in a modding scenario. The symbols + * from human Link's object are used here. + * + * Note that some player forms do not use the eyes and mouth textures loaded into segments 0x08 and 0x09 respectively. + * Therefore, the segment will point at garbage data, but this does not cause issues as the data is not read from. + */ +#ifndef AVOID_UB +static TexturePtr sEyeTextures[PLAYER_EYES_MAX] = { + gLinkHumanEyesOpenTex, // PLAYER_EYES_OPEN + gLinkHumanEyesHalfTex, // PLAYER_EYES_HALF + gLinkHumanEyesClosedTex, // PLAYER_EYES_CLOSED + gLinkHumanEyesRightTex, // PLAYER_EYES_RIGHT + gLinkHumanEyesLeftTex, // PLAYER_EYES_LEFT + gLinkHumanEyesUpTex, // PLAYER_EYES_UP + gLinkHumanEyesDownTex, // PLAYER_EYES_DOWN + gLinkHumanEyesWincingTex, // PLAYER_EYES_WINCING +}; + +static TexturePtr sMouthTextures[PLAYER_MOUTH_MAX] = { gLinkHumanMouthClosedTex, // PLAYER_MOUTH_CLOSED - gLinkHumanMouthTeethTex, // PLAYER_MOUTH_TEETH - gLinkHumanMouthAngryTex, // PLAYER_MOUTH_ANGRY - gLinkHumanMouthHappyTex, // PLAYER_MOUTH_HAPPY -}; - -typedef struct PlayerFaceIndices { - /* 0x0 */ u8 eyeIndex; - /* 0x1 */ u8 mouthIndex; -} PlayerFaceIndices; // size = 0x2 - -PlayerFaceIndices sPlayerFaces[] = { - { PLAYER_EYES_OPEN, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_0 - { PLAYER_EYES_HALF, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_1 - { PLAYER_EYES_CLOSED, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_2 - { PLAYER_EYES_OPEN, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_3 - { PLAYER_EYES_HALF, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_4 - { PLAYER_EYES_CLOSED, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_5 - { PLAYER_EYES_ROLL_LEFT, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_6 - { PLAYER_EYES_ROLL_UP, PLAYER_MOUTH_TEETH }, // PLAYER_FACE_7 - { PLAYER_EYES_7, PLAYER_MOUTH_ANGRY }, // PLAYER_FACE_8 - { PLAYER_EYES_OPEN, PLAYER_MOUTH_ANGRY }, // PLAYER_FACE_9 - { PLAYER_EYES_ROLL_RIGHT, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_10 - { PLAYER_EYES_ROLL_LEFT, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_11 - { PLAYER_EYES_CLOSED, PLAYER_MOUTH_ANGRY }, // PLAYER_FACE_12 - { PLAYER_EYES_HALF, PLAYER_MOUTH_TEETH }, // PLAYER_FACE_13 - { PLAYER_EYES_OPEN, PLAYER_MOUTH_ANGRY }, // PLAYER_FACE_14 - { PLAYER_EYES_OPEN, PLAYER_MOUTH_HAPPY }, // PLAYER_FACE_15 + gLinkHumanMouthHalfTex, // PLAYER_MOUTH_HALF + gLinkHumanMouthOpenTex, // PLAYER_MOUTH_OPEN + gLinkHumanMouthSmileTex, // PLAYER_MOUTH_SMILE +}; +#else +static TexturePtr sEyeTextures[PLAYER_FORM_MAX][PLAYER_EYES_MAX] = { + // PLAYER_FORM_FIERCE_DEITY + { + NULL, // PLAYER_EYES_OPEN + NULL, // PLAYER_EYES_HALF + NULL, // PLAYER_EYES_CLOSED + NULL, // PLAYER_EYES_RIGHT + NULL, // PLAYER_EYES_LEFT + NULL, // PLAYER_EYES_UP + NULL, // PLAYER_EYES_DOWN + NULL, // PLAYER_EYES_WINCING + }, + // PLAYER_FORM_GORON + // Note: use PLAYER_EYES_WINCING to access `gLinkGoronEyesSurprisedTex`. See `Player_DrawImpl`. + { + gLinkGoronEyesOpenTex, // PLAYER_EYES_OPEN + gLinkGoronEyesHalfTex, // PLAYER_EYES_HALF + gLinkGoronEyesClosedTex, // PLAYER_EYES_CLOSED + gLinkGoronEyesSurprisedTex, // PLAYER_EYES_RIGHT + NULL, // PLAYER_EYES_LEFT + NULL, // PLAYER_EYES_UP + NULL, // PLAYER_EYES_DOWN + NULL, // PLAYER_EYES_WINCING + }, + // PLAYER_FORM_ZORA + { + gLinkZoraEyesOpenTex, // PLAYER_EYES_OPEN + gLinkZoraEyesHalfTex, // PLAYER_EYES_HALF + gLinkZoraEyesClosedTex, // PLAYER_EYES_CLOSED + gLinkZoraEyesRightTex, // PLAYER_EYES_RIGHT + gLinkZoraEyesLeftTex, // PLAYER_EYES_LEFT + gLinkZoraEyesUpTex, // PLAYER_EYES_UP + gLinkZoraEyesDownTex, // PLAYER_EYES_DOWN + gLinkZoraEyesWincingTex, // PLAYER_EYES_WINCING + }, + // PLAYER_FORM_DEKU + { + NULL, // PLAYER_EYES_OPEN + NULL, // PLAYER_EYES_HALF + NULL, // PLAYER_EYES_CLOSED + NULL, // PLAYER_EYES_RIGHT + NULL, // PLAYER_EYES_LEFT + NULL, // PLAYER_EYES_UP + NULL, // PLAYER_EYES_DOWN + NULL, // PLAYER_EYES_WINCING + }, + // PLAYER_FORM_HUMAN + { + gLinkHumanEyesOpenTex, // PLAYER_EYES_OPEN + gLinkHumanEyesHalfTex, // PLAYER_EYES_HALF + gLinkHumanEyesClosedTex, // PLAYER_EYES_CLOSED + gLinkHumanEyesRightTex, // PLAYER_EYES_RIGHT + gLinkHumanEyesLeftTex, // PLAYER_EYES_LEFT + gLinkHumanEyesUpTex, // PLAYER_EYES_UP + gLinkHumanEyesDownTex, // PLAYER_EYES_DOWN + gLinkHumanEyesWincingTex, // PLAYER_EYES_WINCING + }, +}; + +static TexturePtr sMouthTextures[PLAYER_FORM_MAX][PLAYER_MOUTH_MAX] = { + // PLAYER_FORM_FIERCE_DEITY + { + NULL, // PLAYER_MOUTH_CLOSED + NULL, // PLAYER_MOUTH_HALF + NULL, // PLAYER_MOUTH_OPEN + NULL, // PLAYER_MOUTH_SMILE + }, + // PLAYER_FORM_GORON + { + NULL, // PLAYER_MOUTH_CLOSED + NULL, // PLAYER_MOUTH_HALF + NULL, // PLAYER_MOUTH_OPEN + NULL, // PLAYER_MOUTH_SMILE + }, + // PLAYER_FORM_ZORA + { + gLinkZoraMouthClosedTex, // PLAYER_MOUTH_CLOSED + gLinkZoraMouthHalfTex, // PLAYER_MOUTH_HALF + gLinkZoraMouthOpenTex, // PLAYER_MOUTH_OPEN + gLinkZoraMouthSmileTex, // PLAYER_MOUTH_SMILE + }, + // PLAYER_FORM_DEKU + { + NULL, // PLAYER_MOUTH_CLOSED + NULL, // PLAYER_MOUTH_HALF + NULL, // PLAYER_MOUTH_OPEN + NULL, // PLAYER_MOUTH_SMILE + }, + // PLAYER_FORM_HUMAN + { + gLinkHumanMouthClosedTex, // PLAYER_MOUTH_CLOSED + gLinkHumanMouthHalfTex, // PLAYER_MOUTH_HALF + gLinkHumanMouthOpenTex, // PLAYER_MOUTH_OPEN + gLinkHumanMouthSmileTex, // PLAYER_MOUTH_SMILE + }, +}; +#endif + +PlayerFaceIndices sPlayerFaces[PLAYER_FACE_MAX] = { + // The first 6 faces defined must be default blinking faces. See relevant code in `Player_UpdateCommon`. + { PLAYER_EYES_OPEN, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_NEUTRAL + { PLAYER_EYES_HALF, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_NEUTRAL_BLINKING_HALF + { PLAYER_EYES_CLOSED, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_NEUTRAL_BLINKING_CLOSED + + // This duplicate set of blinking faces is defined because Player will choose between the first and second set + // based on gameplayFrames. See relevant code in `Player_UpdateCommon`. + // This, in theory, allows for psuedo-random variance in the faces used. But in practice, duplicate faces are used. + { PLAYER_EYES_OPEN, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_NEUTRAL_2 + { PLAYER_EYES_HALF, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_NEUTRAL_BLINKING_HALF_2 + { PLAYER_EYES_CLOSED, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_NEUTRAL_BLINKING_CLOSED_2 + + // Additional faces. Most faces are encoded within animations. + { PLAYER_EYES_LEFT, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_LOOK_LEFT + { PLAYER_EYES_UP, PLAYER_MOUTH_HALF }, // PLAYER_FACE_SURPRISED + { PLAYER_EYES_WINCING, PLAYER_MOUTH_OPEN }, // PLAYER_FACE_HURT + { PLAYER_EYES_OPEN, PLAYER_MOUTH_OPEN }, // PLAYER_FACE_GASP + { PLAYER_EYES_RIGHT, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_LOOK_RIGHT + { PLAYER_EYES_LEFT, PLAYER_MOUTH_CLOSED }, // PLAYER_FACE_LOOK_LEFT_2 + { PLAYER_EYES_CLOSED, PLAYER_MOUTH_OPEN }, // PLAYER_FACE_EYES_CLOSED_MOUTH_OPEN + { PLAYER_EYES_HALF, PLAYER_MOUTH_HALF }, // PLAYER_FACE_OPENING + { PLAYER_EYES_OPEN, PLAYER_MOUTH_OPEN }, // PLAYER_FACE_EYES_AND_MOUTH_OPEN + { PLAYER_EYES_OPEN, PLAYER_MOUTH_SMILE }, // PLAYER_FACE_SMILE }; // Note the correct pointer to pass as the jointTable is the jointTable pointer from the SkelAnime struct, not the @@ -1892,25 +2002,41 @@ void Player_DrawImpl(PlayState* play, void** skeleton, Vec3s* jointTable, s32 dL gfx = POLY_OPA_DISP; + // If the eyes index provided by the animation is negative, use the value provided by the `face` argument instead if (eyeIndex < 0) { eyeIndex = sPlayerFaces[face].eyeIndex; } if (playerForm == PLAYER_FORM_GORON) { - if ((eyeIndex >= PLAYER_EYES_ROLL_RIGHT) && (eyeIndex <= PLAYER_EYES_ROLL_DOWN)) { + // Goron does not have the eye textures to look in different directions + if ((eyeIndex >= PLAYER_EYES_RIGHT) && (eyeIndex <= PLAYER_EYES_DOWN)) { eyeIndex = PLAYER_EYES_OPEN; - } else if (eyeIndex == PLAYER_EYES_7) { - eyeIndex = PLAYER_EYES_ROLL_RIGHT; + } else if (eyeIndex == PLAYER_EYES_WINCING) { + // Goron form puts a surpised expression where the eyes-right normally goes + eyeIndex = PLAYER_EYES_RIGHT; } } - gSPSegment(&gfx[0], 0x08, Lib_SegmentedToVirtual(sPlayerEyesTextures[eyeIndex])); + // Only Human, Zora, and Goron will read the eye textures in the head limb display list. + // Fierce Deity and Deku will point this segment to garbage data, but it will be unread from. +#ifndef AVOID_UB + gSPSegment(&gfx[0], 0x08, Lib_SegmentedToVirtual(sEyeTextures[eyeIndex])); +#else + gSPSegment(&gfx[0], 0x08, Lib_SegmentedToVirtual(sEyeTextures[playerForm][eyeIndex])); +#endif + // If the mouth index provided by the animation is negative, use the value provided by the `face` argument instead if (mouthIndex < 0) { mouthIndex = sPlayerFaces[face].mouthIndex; } - gSPSegment(&gfx[1], 0x09, Lib_SegmentedToVirtual(sPlayerMouthTextures[mouthIndex])); + // Only Human and Zora will read the mouth textures in the head limb display list. + // Goron, Fierce Deity, and Deku will point this segment to garbage data, but it will be unread from. +#ifndef AVOID_UB + gSPSegment(&gfx[1], 0x09, Lib_SegmentedToVirtual(sMouthTextures[mouthIndex])); +#else + gSPSegment(&gfx[1], 0x09, Lib_SegmentedToVirtual(sMouthTextures[playerForm][mouthIndex])); +#endif POLY_OPA_DISP = &gfx[2]; |
