Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
304 changes: 304 additions & 0 deletions soh/soh/Enhancements/ExtraModes/CorpseRun.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h"
#include "soh/Enhancements/randomizer/draw.h"
#include "soh/SaveManager.h"
#include "soh/ShipInit.hpp"
#include "z64item.h"

extern "C" {
#include "functions.h"
#include "macros.h"
#include "variables.h"

extern PlayState* gPlayState;
}

static constexpr int32_t CVAR_CORPSE_RUN_DEFAULT = 0;
#define CVAR_CORPSE_RUN_NAME CVAR_ENHANCEMENT("CorpseRun")
#define CVAR_CORPSE_RUN_VALUE CVarGetInteger(CVAR_CORPSE_RUN_NAME, CVAR_CORPSE_RUN_DEFAULT)

namespace CorpseRun {

static constexpr int32_t INVALID_SAVE_SECTION_ID = -1;
static constexpr int32_t VERSION = 1;
static constexpr float COLLECT_DISTANCE = 40.0f;

struct Remnant {
// State
bool dropped = false;
bool active = false;

int16_t sceneNum = -1;
int8_t roomNum = -1;

Vec3f pos = {};
int16_t yaw = 0;

// Resources
int32_t rupees = 0;
int32_t magic = 0;

// Consumables
int32_t bombs = 0;
int32_t arrows = 0;
int32_t sticks = 0;
int32_t nuts = 0;
int32_t seeds = 0;
int32_t bombchus = 0;

// TODO(jperos): Progression items and/or Equipment modes?

void Clear() {
*this = {};
}
bool HasContents() const;
bool IsInRoom() const;
bool IsRecoverable() const {
return dropped && active;
}
bool IsPendingActivation() const {
return dropped && !active;
}
};

static constexpr struct ConsumableDesc {
const char* name;
int32_t Remnant::*field;
int16_t itemId;
} CONSUMABLE_TABLE[] = {
// clang-format off
{ "bombs", &Remnant::bombs, ITEM_BOMB },
{ "arrows", &Remnant::arrows, ITEM_BOW },
{ "sticks", &Remnant::sticks, ITEM_STICK },
{ "nuts", &Remnant::nuts, ITEM_NUT },
{ "seeds", &Remnant::seeds, ITEM_SLINGSHOT },
{ "bombchus", &Remnant::bombchus, ITEM_BOMBCHU }
// clang-format on
};

static Remnant sRemnant;
static GetItemEntry sMysteryItem = GET_ITEM_MYSTERY;

// Remnant member helpers
bool Remnant::HasContents() const {
if (rupees > 0)
return true;
if (magic > 0)
return true;
for (const ConsumableDesc& desc : CONSUMABLE_TABLE) {
if (this->*desc.field > 0)
return true;
}
return false;
}

bool Remnant::IsInRoom() const {
return gPlayState != nullptr && gPlayState->sceneNum == sceneNum && gPlayState->roomCtx.curRoom.num == roomNum;
}

// Save/Load
static void InitSave(bool isDebug) {
sRemnant.Clear();
}

static void SaveFile() {
SaveManager::Instance->SaveFile(gSaveContext.fileNum);
}

static void LoadSave() {
sRemnant.Clear();

SaveManager::Instance->LoadData("dropped", sRemnant.dropped, false);

if (!sRemnant.dropped) {
return;
}

// Loaded remnants should always be recoverable.
sRemnant.active = true;

SaveManager::Instance->LoadData("sceneNum", sRemnant.sceneNum, (int16_t)-1);
SaveManager::Instance->LoadData("roomNum", sRemnant.roomNum, (int8_t)-1);

SaveManager::Instance->LoadStruct("pos", []() {
SaveManager::Instance->LoadData("x", sRemnant.pos.x, 0.0f);
SaveManager::Instance->LoadData("y", sRemnant.pos.y, 0.0f);
SaveManager::Instance->LoadData("z", sRemnant.pos.z, 0.0f);
});

SaveManager::Instance->LoadData("yaw", sRemnant.yaw, (int16_t)0);

SaveManager::Instance->LoadStruct("contents", []() {
SaveManager::Instance->LoadData("rupees", sRemnant.rupees, 0);
SaveManager::Instance->LoadData("magic", sRemnant.magic, 0);
for (const ConsumableDesc& desc : CONSUMABLE_TABLE) {
SaveManager::Instance->LoadData(desc.name, sRemnant.*desc.field, 0);
}
});
}

static void Save(SaveContext* saveContext, int sectionID, bool fullSave) {
SaveManager::Instance->SaveData("dropped", sRemnant.dropped);

if (!sRemnant.dropped) {
return;
}

SaveManager::Instance->SaveData("sceneNum", sRemnant.sceneNum);
SaveManager::Instance->SaveData("roomNum", sRemnant.roomNum);

SaveManager::Instance->SaveStruct("pos", []() {
SaveManager::Instance->SaveData("x", sRemnant.pos.x);
SaveManager::Instance->SaveData("y", sRemnant.pos.y);
SaveManager::Instance->SaveData("z", sRemnant.pos.z);
});

SaveManager::Instance->SaveData("yaw", sRemnant.yaw);

SaveManager::Instance->SaveStruct("contents", []() {
SaveManager::Instance->SaveData("rupees", sRemnant.rupees);
SaveManager::Instance->SaveData("magic", sRemnant.magic);
for (const ConsumableDesc& desc : CONSUMABLE_TABLE) {
SaveManager::Instance->SaveData(desc.name, sRemnant.*desc.field);
}
});
}

static void RegisterSave() {
static int32_t sSaveSectionId = CorpseRun::INVALID_SAVE_SECTION_ID;

// Don't register twice
if (sSaveSectionId != CorpseRun::INVALID_SAVE_SECTION_ID) {
return;
}

SaveManager::Instance->AddInitFunction(InitSave);
SaveManager::Instance->AddLoadFunction("corpseRun", CorpseRun::VERSION, LoadSave);
sSaveSectionId =
SaveManager::Instance->AddSaveFunction("corpseRun", CorpseRun::VERSION, Save, true, SECTION_PARENT_NONE);
}

// Corpse Run Logic
static void Activate(Player* player, PlayState* playState, int32_t respawnFlag) {
if (!sRemnant.dropped) {
return;
}

sRemnant.active = true;
}

static void Drop() {
// Safety in case death hook fires more than once during the same death flow
// If a remnant has been dropped but not activated by Player_Init yet, this is probably the same death
if (sRemnant.IsPendingActivation()) {
return;
}

if (gPlayState == nullptr) {
return;
}

Player* player = GET_PLAYER(gPlayState);
if (player == nullptr) {
return;
}

// Destroy previous remnant
sRemnant.Clear();

sRemnant.dropped = true;
sRemnant.active = false;
sRemnant.sceneNum = gPlayState->sceneNum;
sRemnant.roomNum = gPlayState->roomCtx.curRoom.num;
sRemnant.pos = player->actor.world.pos;
sRemnant.yaw = player->actor.shape.rot.y;

// v1 - consumables only
sRemnant.rupees = gSaveContext.rupees;
sRemnant.magic = gSaveContext.magic;
for (const ConsumableDesc& desc : CONSUMABLE_TABLE) {
sRemnant.*desc.field = AMMO(desc.itemId);
}

if (!sRemnant.HasContents()) {
// Dark Souls behavior: dying again still destroys the previous remnant,
// even if the new death has nothing worth dropping
sRemnant.Clear();
SaveFile();
return;
}

gSaveContext.rupees = 0;
Magic_Reset(gPlayState);
for (const ConsumableDesc& desc : CONSUMABLE_TABLE) {
AMMO(desc.itemId) = 0;
}

SaveFile();
}

static void Recover() {
Rupees_ChangeBy(sRemnant.rupees);
Magic_RequestChange(gPlayState, sRemnant.magic, MAGIC_ADD);
for (const ConsumableDesc& desc : CONSUMABLE_TABLE) {
Inventory_ChangeAmmo(desc.itemId, sRemnant.*desc.field);
}

sRemnant.Clear();

SaveFile();
}

static void Update() {
if (!sRemnant.IsInRoom() || !sRemnant.IsRecoverable()) {
return;
}

Player* player = GET_PLAYER(gPlayState);
if (player == nullptr) {
return;
}

const float dx = player->actor.world.pos.x - sRemnant.pos.x;
const float dy = player->actor.world.pos.y - sRemnant.pos.y;
const float dz = player->actor.world.pos.z - sRemnant.pos.z;
const float distSq = dx * dx + dy * dy + dz * dz;

if (distSq < SQ(COLLECT_DISTANCE)) {
Recover();
}
}

static void Draw() {
if (!sRemnant.IsInRoom() || !sRemnant.IsRecoverable()) {
return;
}

Matrix_Push();

// Cheap bob/spin so it reads as collectible-ish
// Just kind of picking the values out here
const s16 bobAngle = static_cast<s16>(gPlayState->gameplayFrames * 0x400);
const s16 spinAngle = static_cast<s16>(gPlayState->gameplayFrames * 0x200);

const f32 bob = Math_SinS(bobAngle) * 8.0f;

Matrix_Translate(sRemnant.pos.x, sRemnant.pos.y + 40.0f + bob, sRemnant.pos.z, MTXMODE_NEW);
Matrix_RotateY(static_cast<f32>(BINANG_TO_RAD(spinAngle)), MTXMODE_APPLY);
Matrix_Scale(0.35f, 0.35f, 0.35f, MTXMODE_APPLY);

Randomizer_DrawMysteryItem(gPlayState, &sMysteryItem);

Matrix_Pop();
}

static void Register() {
RegisterSave();

COND_HOOK(OnPlayerInit, CVAR_CORPSE_RUN_VALUE, Activate);
COND_HOOK(OnPlayerDeath, CVAR_CORPSE_RUN_VALUE, Drop);
COND_HOOK(OnGameFrameUpdate, CVAR_CORPSE_RUN_VALUE, Update);
COND_HOOK(OnPlayDrawEnd, CVAR_CORPSE_RUN_VALUE, Draw);
}
} // namespace CorpseRun

static RegisterShipInitFunc initFunc(CorpseRun::Register, { CVAR_CORPSE_RUN_NAME });
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ DEFINE_HOOK(OnFlagUnset, (int16_t flagType, int16_t flag));
DEFINE_HOOK(OnSceneSpawnActors, ());
DEFINE_HOOK(OnLinkSkeletonInit, ());
DEFINE_HOOK(OnLinkEquipmentChange, ());
DEFINE_HOOK(OnPlayerInit, (Player * player, PlayState* playState, int32_t respawnFlag));
DEFINE_HOOK(OnPlayerUpdate, ());
DEFINE_HOOK(OnSetDoAction, (uint16_t action));
DEFINE_HOOK(OnPlayerSfx, (u16 sfxId));
Expand All @@ -46,6 +47,7 @@ DEFINE_HOOK(OnTimestamp, (u8 item));
DEFINE_HOOK(OnPlayerBonk, ());
DEFINE_HOOK(OnPlayerSetModels, (Player * player, u8 modelGroup));
DEFINE_HOOK(OnPlayerHealthChange, (int16_t amount));
DEFINE_HOOK(OnPlayerDeath, ());
DEFINE_HOOK(OnPlayerBottleUpdate, (int16_t contents));
DEFINE_HOOK(OnPlayerHoldUpShield, ());
DEFINE_HOOK(OnPlayerFirstPersonControl, (Player * player));
Expand Down
8 changes: 8 additions & 0 deletions soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ void GameInteractor_ExecuteOnLinkEquipmentChange() {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnLinkEquipmentChange>();
}

void GameInteractor_ExecuteOnPlayerInit(Player* player, PlayState* playState, int32_t respawnFlag) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnPlayerInit>(player, playState, respawnFlag);
}

void GameInteractor_ExecuteOnPlayerUpdate() {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnPlayerUpdate>();
}
Expand Down Expand Up @@ -213,6 +217,10 @@ void GameInteractor_ExecuteOnPlayerHealthChange(int16_t amount) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnPlayerHealthChange>(amount);
}

void GameInteractor_ExecuteOnPlayerDeath() {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnPlayerDeath>();
}

void GameInteractor_ExecuteOnPlayerBottleUpdate(int16_t contents) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnPlayerBottleUpdate>(contents);
}
Expand Down
2 changes: 2 additions & 0 deletions soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ void GameInteractor_ExecuteOnFlagUnset(int16_t flagType, int16_t flag);
void GameInteractor_ExecuteOnSceneSpawnActors();
void GameInteractor_ExecuteOnLinkSkeletonInit();
void GameInteractor_ExecuteOnLinkEquipmentChange();
void GameInteractor_ExecuteOnPlayerInit(Player* player, PlayState* playState, int32_t respawnFlag);
void GameInteractor_ExecuteOnPlayerUpdate();
void GameInteractor_ExecuteOnSetDoAction(uint16_t action);
void GameInteractor_ExecuteOnPlayerSfx(u16 sfxId);
Expand All @@ -47,6 +48,7 @@ void GameInteractor_ExecuteOnTimestamp(u8 item);
void GameInteractor_ExecuteOnPlayerBonk();
void GameInteractor_ExecuteOnPlayerSetModels(Player* player, u8 modelGroup);
void GameInteractor_ExecuteOnPlayerHealthChange(int16_t amount);
void GameInteractor_ExecuteOnPlayerDeath();
void GameInteractor_ExecuteOnPlayerBottleUpdate(int16_t contents);
void GameInteractor_ExecuteOnPlayerHoldUpShield();
void GameInteractor_ExecuteOnPlayerFirstPersonControl(Player* player);
Expand Down
4 changes: 4 additions & 0 deletions soh/soh/SohGui/SohMenuEnhancements.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1676,6 +1676,10 @@ void SohMenu::AddMenuEnhancements() {
AddWidget(path, "Shadow Tag Mode", WIDGET_CVAR_CHECKBOX)
.CVar(CVAR_ENHANCEMENT("ShadowTag"))
.Options(CheckboxOptions().Tooltip("A Wallmaster follows Link everywhere, don't get caught!"));
AddWidget(path, "Corpse Run Mode", WIDGET_CVAR_CHECKBOX)
.CVar(CVAR_ENHANCEMENT("CorpseRun"))
.Options(CheckboxOptions().Tooltip("On death, drops all of your consumables at the location of death.\n\n"
"Pick up the remnant before dying again or it goes away!"));
AddWidget(path, "Hurt Container Mode", WIDGET_CVAR_CHECKBOX)
.CVar(CVAR_ENHANCEMENT("HurtContainer"))
.Options(CheckboxOptions().Tooltip("Changes Heart Piece and Heart Container functionality.\n\n"
Expand Down
3 changes: 3 additions & 0 deletions soh/src/overlays/actors/ovl_player_actor/z_player.c
Original file line number Diff line number Diff line change
Expand Up @@ -3538,6 +3538,7 @@ void func_80836448(PlayState* play, Player* this, LinkAnimationHeader* anim) {
Audio_PlayFanfare(NA_BGM_GAME_OVER);
gSaveContext.seqId = (u8)NA_BGM_DISABLED;
gSaveContext.natureAmbienceId = NATURE_ID_DISABLED;
GameInteractor_ExecuteOnPlayerDeath();
}

OnePointCutscene_Init(play, 9806, cond ? 120 : 60, &this->actor, MAIN_CAM);
Expand Down Expand Up @@ -10909,6 +10910,8 @@ void Player_Init(Actor* thisx, PlayState* play2) {

Map_SavePlayerInitialInfo(play);
MREG(64) = 0;

GameInteractor_ExecuteOnPlayerInit(this, play, respawnFlag);
Comment thread
jdperos marked this conversation as resolved.
Outdated
}

void Player_ApproachZeroBinang(s16* pValue) {
Expand Down
Loading