diff --git a/soh/assets/custom/objects/object_penguin/mat_object_penguin_DL_beak b/soh/assets/custom/objects/object_penguin/mat_object_penguin_DL_beak
new file mode 100644
index 000000000..f83feec12
--- /dev/null
+++ b/soh/assets/custom/objects/object_penguin/mat_object_penguin_DL_beak
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/soh/assets/custom/objects/object_penguin/mat_object_penguin_DL_eye b/soh/assets/custom/objects/object_penguin/mat_object_penguin_DL_eye
new file mode 100644
index 000000000..c24abe2ab
--- /dev/null
+++ b/soh/assets/custom/objects/object_penguin/mat_object_penguin_DL_eye
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/soh/assets/custom/objects/object_penguin/mat_object_penguin_DL_skin b/soh/assets/custom/objects/object_penguin/mat_object_penguin_DL_skin
new file mode 100644
index 000000000..6c57863e3
--- /dev/null
+++ b/soh/assets/custom/objects/object_penguin/mat_object_penguin_DL_skin
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/soh/assets/custom/objects/object_penguin/object_penguin_DL b/soh/assets/custom/objects/object_penguin/object_penguin_DL
new file mode 100644
index 000000000..393bbc438
--- /dev/null
+++ b/soh/assets/custom/objects/object_penguin/object_penguin_DL
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/soh/assets/custom/objects/object_penguin/object_penguin_DL_tri_0 b/soh/assets/custom/objects/object_penguin/object_penguin_DL_tri_0
new file mode 100644
index 000000000..9039650d4
--- /dev/null
+++ b/soh/assets/custom/objects/object_penguin/object_penguin_DL_tri_0
@@ -0,0 +1,862 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/soh/assets/custom/objects/object_penguin/object_penguin_DL_tri_1 b/soh/assets/custom/objects/object_penguin/object_penguin_DL_tri_1
new file mode 100644
index 000000000..efb326ace
--- /dev/null
+++ b/soh/assets/custom/objects/object_penguin/object_penguin_DL_tri_1
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/soh/assets/custom/objects/object_penguin/object_penguin_DL_tri_2 b/soh/assets/custom/objects/object_penguin/object_penguin_DL_tri_2
new file mode 100644
index 000000000..96c7933ec
--- /dev/null
+++ b/soh/assets/custom/objects/object_penguin/object_penguin_DL_tri_2
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/soh/assets/custom/objects/object_penguin/object_penguin_DL_vtx_0 b/soh/assets/custom/objects/object_penguin/object_penguin_DL_vtx_0
new file mode 100644
index 000000000..f1a650d47
--- /dev/null
+++ b/soh/assets/custom/objects/object_penguin/object_penguin_DL_vtx_0
@@ -0,0 +1,922 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/soh/assets/custom/objects/object_penguin/object_penguin_DL_vtx_1 b/soh/assets/custom/objects/object_penguin/object_penguin_DL_vtx_1
new file mode 100644
index 000000000..5cd519f95
--- /dev/null
+++ b/soh/assets/custom/objects/object_penguin/object_penguin_DL_vtx_1
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/soh/assets/custom/objects/object_penguin/object_penguin_DL_vtx_2 b/soh/assets/custom/objects/object_penguin/object_penguin_DL_vtx_2
new file mode 100644
index 000000000..97f03bf0f
--- /dev/null
+++ b/soh/assets/custom/objects/object_penguin/object_penguin_DL_vtx_2
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/soh/assets/custom/objects/object_penguin/ping_eye b/soh/assets/custom/objects/object_penguin/ping_eye
new file mode 100644
index 000000000..32eb2e7e1
Binary files /dev/null and b/soh/assets/custom/objects/object_penguin/ping_eye differ
diff --git a/soh/assets/custom/objects/object_penguin/ping_tex b/soh/assets/custom/objects/object_penguin/ping_tex
new file mode 100644
index 000000000..9913decc7
Binary files /dev/null and b/soh/assets/custom/objects/object_penguin/ping_tex differ
diff --git a/soh/assets/soh_assets.h b/soh/assets/soh_assets.h
index 5ea41262a..50a5df2eb 100644
--- a/soh/assets/soh_assets.h
+++ b/soh/assets/soh_assets.h
@@ -137,6 +137,9 @@ static const ALIGN_ASSET(2) char gXmasDecor100DL[] = dgXmasDecor100DL;
#define dgXmasStarDL "__OTR__objects/object_xmas_tree/gXmasStarDL"
static const ALIGN_ASSET(2) char gXmasStarDL[] = dgXmasStarDL;
+#define dgPenguinDL "__OTR__objects/object_penguin/object_penguin_DL"
+static const ALIGN_ASSET(2) char gPenguinDL[] = dgPenguinDL;
+
#define dgKakarikoDecorDL "__OTR__objects/object_kakariko_decor/gKakarikoDecorDL"
static const ALIGN_ASSET(2) char gKakarikoDecorDL[] = dgKakarikoDecorDL;
diff --git a/soh/soh/Enhancements/Holiday/AGreenSpoon.cpp b/soh/soh/Enhancements/Holiday/AGreenSpoon.cpp
new file mode 100644
index 000000000..2e745a9ee
--- /dev/null
+++ b/soh/soh/Enhancements/Holiday/AGreenSpoon.cpp
@@ -0,0 +1,81 @@
+#include "Holiday.hpp"
+#include "soh/Enhancements/randomizer/3drando/random.hpp"
+#include "soh/frame_interpolation.h"
+#include "soh_assets.h"
+#include "overlays/actors/ovl_En_Gs/z_en_gs.h"
+#include "overlays/actors/ovl_En_Clear_Tag/z_en_clear_tag.h"
+
+extern "C" {
+#include "macros.h"
+#include "functions.h"
+#include "variables.h"
+extern PlayState* gPlayState;
+}
+
+#define AUTHOR "AGreenSpoon"
+#define CVAR(v) "gHoliday." AUTHOR "." v
+
+void EnGs_Evil(EnGs* enGs, PlayState* play) {
+ Player* player = GET_PLAYER(gPlayState);
+ if (!(player->stateFlags1 & PLAYER_STATE1_TALKING)) {
+ Math_ApproachS(&enGs->actor.shape.rot.y, enGs->actor.yawTowardsPlayer, 5, 0xBB8);
+
+ if (enGs->unk_200 <= 0) {
+ float offsetDistance = 10.0f;
+ float offsetX = sinf(enGs->actor.shape.rot.y * (M_PI / 0x8000)) * offsetDistance;
+ float offsetZ = cosf(enGs->actor.shape.rot.y * (M_PI / 0x8000)) * offsetDistance;
+
+ float dx = player->actor.world.pos.x - (enGs->actor.world.pos.x + offsetX);
+ float dy = player->actor.world.pos.y - 10.0f - enGs->actor.world.pos.y;
+ float dz = player->actor.world.pos.z - (enGs->actor.world.pos.z + offsetZ);
+
+ s16 rotX = atan2f(dy, sqrtf(dx * dx + dz * dz)) * (0x8000 / M_PI);
+ s16 rotY = enGs->actor.shape.rot.y;
+ s16 rotZ = atan2f(dx, dz) * (0x8000 / M_PI);
+
+ Actor* actor = Actor_Spawn(&play->actorCtx, play, ACTOR_EN_CLEAR_TAG,
+ enGs->actor.world.pos.x + offsetX,
+ enGs->actor.world.pos.y + 40.0f,
+ enGs->actor.world.pos.z + offsetZ,
+ rotX, rotY, rotZ,
+ 100, false);
+
+ EnClearTag* clearTag = (EnClearTag*)actor;
+
+ enGs->unk_200 = 5;
+ }
+
+ enGs->unk_200--;
+ }
+}
+
+static void OnConfigurationChanged() {
+ COND_ID_HOOK(OnOpenText, 0x2053, CVarGetInteger(CVAR("EvilGossipStone"), 0), [](u16 * textId, bool* loadFromMessageTable) {
+ Actor* actor = Actor_FindNearby(gPlayState, &GET_PLAYER(gPlayState)->actor, ACTOR_EN_GS, ACTORCAT_PROP, 100.0f);
+
+ if (actor == NULL) {
+ return;
+ }
+
+ EnGs* gs = (EnGs*)actor;
+ gs->actionFunc = EnGs_Evil;
+ });
+}
+
+static void DrawMenu() {
+ ImGui::SeparatorText(AUTHOR);
+ if (UIWidgets::EnhancementCheckbox("Evil Gossip Stone", CVAR("EvilGossipStone"))) {
+ OnConfigurationChanged();
+ }
+}
+
+static void RegisterMod() {
+ // #region Leave this alone unless you know what you are doing
+ OnConfigurationChanged();
+ // #endregion
+
+ // TODO: Anything you want to run once on startup
+}
+
+// TODO: Uncomment this line to enable the mod
+static Holiday holiday(DrawMenu, RegisterMod);
diff --git a/soh/soh/Enhancements/Holiday/Example.cpp b/soh/soh/Enhancements/Holiday/Example.cpp
new file mode 100644
index 000000000..235738811
--- /dev/null
+++ b/soh/soh/Enhancements/Holiday/Example.cpp
@@ -0,0 +1,45 @@
+#include "Holiday.hpp"
+
+extern "C" {
+#include "macros.h"
+#include "functions.h"
+#include "variables.h"
+extern PlayState* gPlayState;
+
+// TODO: Include anything you need here from C land
+}
+
+// TODO: Change this to YourName
+#define AUTHOR "Example"
+#define CVAR(v) "gHoliday." AUTHOR "." v
+
+static void OnConfigurationChanged() {
+ // TODO: Register any hooks or things that need to run on startup and when the main CVar is toggled
+ // Note: Hooks should be registered/unregistered depending on the CVar state (Use COND_HOOK or COND_ID_HOOK)
+
+ // COND_HOOK(OnSceneSpawnActors, CVarGetInteger(CVAR("Enabled"), 0), []() {
+ // // Spawn your own actors?
+ // });
+
+ // COND_ID_HOOK(OnActorInit, ACTOR_OBJ_TSUBO, CVarGetInteger(CVAR("DoSomethingWithPots"), 0), [](void* actorRef) {
+ // // Do something with pots?
+ // });
+}
+
+static void DrawMenu() {
+ ImGui::SeparatorText(AUTHOR);
+ if (UIWidgets::EnhancementCheckbox("DoSomethingWithPots", CVAR("DoSomethingWithPots"))) {
+ OnConfigurationChanged();
+ }
+}
+
+static void RegisterMod() {
+ // #region Leave this alone unless you know what you are doing
+ OnConfigurationChanged();
+ // #endregion
+
+ // TODO: Anything you want to run once on startup
+}
+
+// TODO: Uncomment this line to enable the mod
+// static Holiday holiday(DrawMenu, RegisterMod);
diff --git a/soh/soh/Enhancements/Holiday/Fredomato.cpp b/soh/soh/Enhancements/Holiday/Fredomato.cpp
new file mode 100644
index 000000000..5ac148c2d
--- /dev/null
+++ b/soh/soh/Enhancements/Holiday/Fredomato.cpp
@@ -0,0 +1,155 @@
+#include "Holiday.hpp"
+#include
+#include "soh/UIWidgets.hpp"
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "objects/object_dog/object_dog.h"
+#include "soh/frame_interpolation.h"
+#include "soh/Enhancements/randomizer/3drando/random.hpp"
+#include "soh/Enhancements/randomizer/3drando/location_access.hpp"
+#include "soh/Enhancements/randomizer/entrance.h"
+
+#include "objects/gameplay_field_keep/gameplay_field_keep.h"
+#include "objects/object_md/object_md.h"
+#include "src/overlays/actors/ovl_Door_Ana/z_door_ana.h"
+extern "C" {
+#include "macros.h"
+#include "functions.h"
+#include "variables.h"
+
+extern PlayState* gPlayState;
+void DoorAna_SetupAction(DoorAna* doorAna, DoorAnaActionFunc actionFunc);
+void DoorAna_GrabPlayer(DoorAna* doorAna, PlayState* play);
+}
+
+#define AUTHOR "Fredomato"
+#define CVAR(v) "gHoliday." AUTHOR "." v
+
+static CollisionPoly snowballPoly;
+static f32 raycastResult;
+
+const s16 entrances[] = {
+ 0x0000, 0x0209, 0x0004, 0x0242, 0x0028, 0x0221, 0x0169, 0x0215, 0x0165, 0x024A, 0x0010, 0x021D, 0x0082, 0x01E1, 0x0037, 0x0205,
+ 0x0098, 0x02A6, 0x0088, 0x03D4, 0x0008, 0x03A8, 0x0467, 0x023D, 0x0433, 0x0443, 0x0437, 0x0447, 0x009C, 0x033C, 0x00C9, 0x026A,
+ 0x00C1, 0x0266, 0x0043, 0x03CC, 0x045F, 0x0309, 0x03A0, 0x03D0, 0x007E, 0x026E, 0x0530, 0x01D1, 0x0507, 0x03BC, 0x0388, 0x02A2,
+ 0x0063, 0x01D5, 0x0528, 0x03C0, 0x043B, 0x0067, 0x02FD, 0x0349, 0x0550, 0x04EE, 0x039C, 0x0345, 0x05C8, 0x05DC, 0x0072, 0x034D,
+ 0x030D, 0x0355, 0x037C, 0x03FC, 0x0380, 0x03C4, 0x004F, 0x0378, 0x02F9, 0x042F, 0x05D0, 0x05D4, 0x052C, 0x03B8, 0x016D, 0x01CD,
+ 0x00B7, 0x0201, 0x003B, 0x0463, 0x0588, 0x057C, 0x0578, 0x0340, 0x04C2, 0x03E8, 0x04BE, 0x0482, 0x0315, 0x045B, 0x0371, 0x0394,
+ 0x0272, 0x0211, 0x0053, 0x0472, 0x0453, 0x0351, 0x0384, 0x044B, 0x03EC, 0x04FF, 0x0700, 0x0800, 0x0701, 0x0801, 0x0702, 0x0802,
+ 0x0703, 0x0803, 0x0704, 0x0804, 0x0705, 0x0805, 0x0706, 0x0806, 0x0707, 0x0807, 0x0708, 0x0808, 0x0709, 0x0809, 0x070A, 0x080A,
+ 0x070B, 0x080B, 0x070C, 0x080C, 0x070D, 0x080D, 0x070E, 0x080E, 0x070F, 0x080F, 0x0710, 0x0711, 0x0811, 0x0712, 0x0812,
+ 0x0713, 0x0813, 0x0714, 0x0814, 0x0715, 0x0815, 0x0716, 0x0816, 0x0717, 0x0817, 0x0718, 0x0818, 0x0719, 0x0819, 0x081A,
+ 0x071B, 0x081B, 0x071C, 0x081C, 0x071D, 0x081D, 0x071E, 0x081E, 0x071F, 0x081F, 0x0720, 0x0820, 0x004B, 0x035D, 0x031C, 0x0361,
+ 0x002D, 0x050B, 0x044F, 0x0359, 0x05E0, 0x020D, 0x011E, 0x0286, 0x04E2, 0x04D6, 0x01DD, 0x04DA, 0x00FC, 0x01A9, 0x0185, 0x04DE,
+ 0x0102, 0x0189, 0x0117, 0x018D, 0x0276, 0x01FD, 0x00DB, 0x017D, 0x00EA, 0x0181, 0x0157, 0x01F9, 0x0328, 0x0560, 0x0129, 0x022D,
+ 0x0130, 0x03AC, 0x0123, 0x0365, 0x00B1, 0x0033, 0x0138, 0x025A, 0x0171, 0x025E, 0x00E4, 0x0195, 0x013D, 0x0191, 0x014D, 0x01B9,
+ 0x0246, 0x01C1, 0x0147, 0x01BD, 0x0108, 0x019D, 0x0225, 0x01A1, 0x0219, 0x027E, 0x0554, 0x00BB, 0x0282, 0x0600, 0x04F6, 0x0604,
+ 0x01F1, 0x0568, 0x05F4, 0x040F, 0x0252, 0x040B, 0x00C5, 0x0301, 0x0407, 0x000C, 0x024E, 0x0305, 0x0175, 0x0417, 0x0423, 0x008D,
+ 0x02F5, 0x0413, 0x02B2, 0x0457, 0x047A, 0x010E, 0x0608, 0x0564, 0x060C, 0x0610, 0x0580
+};
+
+static bool midoGrottoInit = false;
+static SkelAnime midoSkelAnime;
+static Vec3s midoJointTable[17];
+static Vec3s midoMorphTable[17];
+
+static void RandomGrotto_WaitOpen(DoorAna* doorAna, PlayState* play) {
+ if (!midoGrottoInit) {
+ midoGrottoInit = true;
+ SkelAnime_InitFlex(play, &midoSkelAnime, (FlexSkeletonHeader*)&gMidoSkel, (AnimationHeader*)&gMidoWalkingAnim, midoJointTable, midoMorphTable, 17);
+ }
+ SkelAnime_Update(&midoSkelAnime);
+
+ Actor* actor = &doorAna->actor;
+ Player* player = GET_PLAYER(play);
+ Math_SmoothStepToF(&actor->world.pos.x, player->actor.world.pos.x, 0.1f, 10.0f, 0.0f);
+ Math_SmoothStepToF(&actor->world.pos.z, player->actor.world.pos.z, 0.1f, 10.0f, 0.0f);
+ Math_SmoothStepToF(&actor->world.pos.y, player->actor.world.pos.y, 0.1f, 10.0f, 0.0f);
+
+ Math_ApproachS(&doorAna->actor.shape.rot.y, doorAna->actor.yawTowardsPlayer, 5, 0xBB8);
+
+ if (Math_StepToF(&actor->scale.x, 0.01f, 0.001f)) {
+ if ((actor->targetMode != 0) && (play->transitionTrigger == TRANS_TRIGGER_OFF) && (player->stateFlags1 & PLAYER_STATE1_FLOOR_DISABLED) && (player->av1.actionVar1 == 0)) {
+ play->nextEntranceIndex = RandomElement(entrances);
+ DoorAna_SetupAction((DoorAna*)actor, DoorAna_GrabPlayer);
+ } else {
+ if (!Player_InCsMode(play) && !(player->stateFlags1 & (PLAYER_STATE1_ON_HORSE | PLAYER_STATE1_IN_WATER)) &&
+ actor->xzDistToPlayer <= 15.0f && -50.0f <= actor->yDistToPlayer &&
+ actor->yDistToPlayer <= 15.0f) {
+ player->stateFlags1 |= PLAYER_STATE1_FLOOR_DISABLED;
+ actor->targetMode = 1;
+ } else {
+ actor->targetMode = 0;
+ }
+ }
+ }
+ Actor_SetScale(actor, actor->scale.x);
+}
+
+static void RandomGrotto_Draw(Actor* actor, PlayState* play) {
+ if (!midoGrottoInit) {
+ return;
+ }
+ OPEN_DISPS(play->state.gfxCtx);
+
+ Gfx_SetupDL_25Xlu(play->state.gfxCtx);
+ gSPMatrix(POLY_XLU_DISP++, MATRIX_NEWMTX(play->state.gfxCtx),
+ G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW);
+ gSPDisplayList(POLY_XLU_DISP++, (Gfx*)gGrottoDL);
+
+ Matrix_Translate(0.0f, -2700.0f, 0.0f, MTXMODE_APPLY);
+ gSPMatrix(POLY_OPA_DISP++, Matrix_NewMtx(play->state.gfxCtx, (char*)__FILE__, __LINE__), G_MTX_MODELVIEW | G_MTX_LOAD);
+ gSPSegment(POLY_OPA_DISP++, 0x08, (uintptr_t)gMidoEyeOpenTex);
+ func_80034BA0(play, &midoSkelAnime, NULL, NULL, actor, 255);
+
+ CLOSE_DISPS(play->state.gfxCtx);
+}
+
+static void SpawnRandomGrotto() {
+ Vec3f pos;
+ pos.y = 9999.0f;
+ int spawnAttempts = 0;
+ while (spawnAttempts < 50) {
+ if (GET_PLAYER(gPlayState) != nullptr) {
+ pos.x = GET_PLAYER(gPlayState)->actor.world.pos.x;
+ pos.z = GET_PLAYER(gPlayState)->actor.world.pos.z;
+ } else {
+ pos.x = 0;
+ pos.z = 0;
+ }
+ // X/Z anywhere from -1000.0 to +1000.0 from player
+ pos.x += (float)(Random(0, 2000)) - 1000.0f;
+ pos.z += (float)(Random(0, 2000)) - 1000.0f;
+
+ raycastResult = BgCheck_AnyRaycastFloor1(&gPlayState->colCtx, &snowballPoly, &pos);
+
+ if (raycastResult > BGCHECK_Y_MIN) {
+ Actor* grotto = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_DOOR_ANA, pos.x, raycastResult, pos.z, 0, 0, 0, 0, false);
+ midoGrottoInit = false;
+ DoorAna_SetupAction((DoorAna*)grotto, RandomGrotto_WaitOpen);
+ grotto->draw = RandomGrotto_Draw;
+ break;
+ }
+
+ spawnAttempts++;
+ }
+}
+
+static void ConfigurationChanged() {
+ COND_HOOK(OnSceneSpawnActors, CVarGetInteger(CVAR("KrampusHole"), 0), SpawnRandomGrotto);
+}
+
+static void DrawMenu() {
+ ImGui::SeparatorText(AUTHOR);
+
+ if (UIWidgets::EnhancementCheckbox("The Krampus Hole", CVAR("KrampusHole"))) {
+ ConfigurationChanged();
+ }
+}
+
+static void RegisterMod() {
+ // #region Leave this alone unless you know what you are doing
+ ConfigurationChanged();
+ // #endregion
+}
+
+static Holiday holiday(DrawMenu, RegisterMod);
diff --git a/soh/soh/Enhancements/Holiday/Grimey.cpp b/soh/soh/Enhancements/Holiday/Grimey.cpp
new file mode 100644
index 000000000..cd21ce91c
--- /dev/null
+++ b/soh/soh/Enhancements/Holiday/Grimey.cpp
@@ -0,0 +1,208 @@
+#include "Holiday.hpp"
+#include "soh/Enhancements/randomizer/3drando/random.hpp"
+#include "soh/frame_interpolation.h"
+#include "soh_assets.h"
+#include "overlays/actors/ovl_En_Nutsball/z_en_nutsball.h"
+
+extern "C" {
+#include "macros.h"
+#include "functions.h"
+#include "variables.h"
+
+#include "objects/gameplay_field_keep/gameplay_field_keep.h"
+extern PlayState* gPlayState;
+void func_80ABBBA8(EnNutsball* nut, PlayState* play);
+void EnNutsball_Draw(Actor* nut, PlayState* play);
+}
+
+#define AUTHOR "Grimey"
+#define CVAR(v) "gHoliday." AUTHOR "." v
+
+static bool spawningPenguins = false;
+
+typedef enum {
+ PENGUIN_STATE_IDLE,
+ PENGUIN_STATE_WALK,
+} PenguinState;
+
+struct Penguin {
+ PenguinState state;
+ s16 timer;
+ s16 targetRot;
+};
+
+std::unordered_map penguins;
+
+void Penguin_Init(Actor* actor, PlayState* play) {
+ Penguin penguin;
+ penguin.state = PENGUIN_STATE_IDLE;
+ penguin.timer = 0;
+ actor->world.rot.y = penguin.targetRot = rand() % 0x10000;
+ penguins[actor] = penguin;
+ actor->gravity = -1.0f;
+ actor->flags &= ~ACTOR_FLAG_TARGETABLE;
+}
+
+void Penguin_Update(Actor* actor, PlayState* play) {
+ Penguin* penguin = &penguins[actor];
+
+ if (penguin->timer <= 0) {
+ if (penguin->state == PENGUIN_STATE_IDLE) {
+ penguin->state = (PenguinState)(rand() % 3);
+ penguin->timer = rand() % (20 * 10) + (20 * 3);
+ } else {
+ penguin->state = PENGUIN_STATE_IDLE;
+ penguin->timer = rand() % (20 * 10) + (20 * 3);
+ }
+ } else {
+ penguin->timer--;
+ }
+
+ if (rand() % 100 == 0) {
+ penguin->targetRot = rand() % 0x10000;
+ }
+
+ switch (penguin->state) {
+ case PENGUIN_STATE_IDLE:
+ break;
+ case PENGUIN_STATE_WALK:
+ actor->speedXZ = 0.5f;
+ break;
+ }
+
+ Math_SmoothStepToS(&actor->world.rot.y, penguin->targetRot, 1, 200, 0);
+ actor->shape.rot.y = actor->world.rot.y;
+
+ if (actor->speedXZ < 0.0f) {
+ actor->speedXZ = 0.0f;
+ }
+
+ Actor_MoveForward(actor);
+
+ Actor_UpdateBgCheckInfo(play, actor, 10.0f, 10.0f, 0.0f, 0xFF);
+}
+
+void Penguin_Draw(Actor* actor, PlayState* play) {
+ OPEN_DISPS(play->state.gfxCtx);
+
+ Gfx_SetupDL_25Opa(play->state.gfxCtx);
+
+ Matrix_Scale(0.8f, 0.8f, 0.8f, MTXMODE_APPLY);
+ Matrix_Translate(0, 2000.0f, 0, MTXMODE_APPLY);
+ gSPMatrix(POLY_OPA_DISP++, Matrix_NewMtx(play->state.gfxCtx, (char*)__FILE__, __LINE__), G_MTX_MODELVIEW | G_MTX_LOAD);
+ gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, 255);
+ gSPDisplayList(POLY_OPA_DISP++, (Gfx*)gPenguinDL);
+
+ CLOSE_DISPS(play->state.gfxCtx);
+}
+
+void Penguin_Destroy(Actor* actor, PlayState* play) {
+ penguins.erase(actor);
+}
+
+static void OnConfigurationChanged() {
+ COND_HOOK(OnPlayerUpdate, CVarGetInteger(CVAR("Hailstorm"), 0), []() {
+ // Every frame has a 1/300 chance of spawning hail
+ if (rand() % 300 == 0) {
+ int spawned = 0;
+ while (spawned < 1) {
+ Vec3f pos = GET_PLAYER(gPlayState)->actor.world.pos;
+ pos.x += (float)Random(0, 100) - 50.0f;
+ pos.z += (float)Random(0, 100) - 50.0f;
+ pos.y += 200.0f;
+
+ Actor* actor = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_EN_NUTSBALL, pos.x, pos.y, pos.z, 0, 0, 0, 0, false);
+ EnNutsball* nut = (EnNutsball*)actor;
+ nut->actor.draw = EnNutsball_Draw;
+ nut->actor.shape.rot.y = 0;
+ nut->timer = 0;
+ nut->actionFunc = func_80ABBBA8;
+ nut->actor.speedXZ = 0.0f;
+ nut->actor.gravity = -2.0f;
+ spawned++;
+ }
+ }
+ });
+
+ COND_HOOK(OnSceneSpawnActors, CVarGetInteger(CVAR("Penguins"), 0), []() {
+ penguins.clear();
+
+ if (gPlayState->sceneNum != SCENE_HYRULE_FIELD) {
+ return;
+ }
+
+ static Vec3f huddlePos;
+ static Vec3f spawnPos;
+ static f32 raycastResult;
+ static CollisionPoly poly;
+
+ spawningPenguins = true;
+
+ int huddlesSpawned = 0;
+ while (huddlesSpawned < 10) {
+ huddlePos.x = (float)(Random(
+ (gPlayState->sceneNum == SCENE_HYRULE_FIELD ? -10000 : -2700) + 10000,
+ (gPlayState->sceneNum == SCENE_HYRULE_FIELD ? 5000 : 2000) + 10000
+ ) - (float)10000.0f);
+ huddlePos.y = 5000;
+ huddlePos.z = (float)(Random(
+ (gPlayState->sceneNum == SCENE_HYRULE_FIELD ? -1000 : -2000) + 10000,
+ (gPlayState->sceneNum == SCENE_HYRULE_FIELD ? 15000 : 2000) + 10000
+ ) - (float)10000.0f);
+
+ if (BgCheck_AnyRaycastFloor1(&gPlayState->colCtx, &poly, &huddlePos) <= BGCHECK_Y_MIN) {
+ continue;
+ }
+
+ // 5-10
+ int huddleSize = rand() % 6 + 5;
+ int penguinsSpawned = 0;
+ while (penguinsSpawned < huddleSize) {
+ spawnPos.x = huddlePos.x + rand() % 100 - 50;
+ spawnPos.y = huddlePos.y;
+ spawnPos.z = huddlePos.z + rand() % 100 - 50;
+
+ raycastResult = BgCheck_AnyRaycastFloor1(&gPlayState->colCtx, &poly, &spawnPos);
+
+ if (raycastResult > BGCHECK_Y_MIN) {
+ Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_EN_OE2, spawnPos.x, raycastResult, spawnPos.z, 0, 0, 0, 0, false);
+ penguinsSpawned++;
+ }
+ }
+ huddlesSpawned++;
+ }
+
+ spawningPenguins = false;
+ });
+
+ COND_ID_HOOK(ShouldActorInit, ACTOR_EN_OE2, CVarGetInteger(CVAR("Penguins"), 0), [](void* actorRef, bool* should) {
+ Actor* actor = (Actor*)actorRef;
+ if (spawningPenguins) {
+ actor->init = Penguin_Init;
+ actor->update = Penguin_Update;
+ actor->draw = Penguin_Draw;
+ actor->destroy = Penguin_Destroy;
+ }
+ });
+}
+
+static void DrawMenu() {
+ ImGui::SeparatorText(AUTHOR);
+ if (UIWidgets::EnhancementCheckbox("Penguins", CVAR("Penguins"))) {
+ OnConfigurationChanged();
+ }
+ if (UIWidgets::EnhancementCheckbox("Hailstorm", CVAR("Hailstorm"))) {
+ OnConfigurationChanged();
+ }
+}
+
+static void RegisterMod() {
+ // #region Leave this alone unless you know what you are doing
+ OnConfigurationChanged();
+ // #endregion
+
+ // TODO: Anything you want to run once on startup
+}
+
+// TODO: Uncomment this line to enable the mod
+static Holiday holiday(DrawMenu, RegisterMod);
diff --git a/soh/soh/Enhancements/Holiday/Holiday.hpp b/soh/soh/Enhancements/Holiday/Holiday.hpp
new file mode 100644
index 000000000..9a6717060
--- /dev/null
+++ b/soh/soh/Enhancements/Holiday/Holiday.hpp
@@ -0,0 +1,38 @@
+#ifndef HOLIDAY_HPP
+#define HOLIDAY_HPP
+
+#include
+#include
+#include
+#include "soh/UIWidgets.hpp"
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/Enhancements/cosmetics/CosmeticsEditor.h"
+
+inline std::vector> holidayDrawFuncs = {};
+inline std::vector> holidayRegisterFuncs = {};
+
+inline void DrawHolidayMenu() {
+ if (ImGui::BeginMenu("Holiday")) {
+ for (auto& drawFunc : holidayDrawFuncs) {
+ ImGui::PushID(&drawFunc);
+ drawFunc();
+ ImGui::PopID();
+ }
+ ImGui::EndMenu();
+ }
+}
+
+inline void RegisterHoliday() {
+ for (auto& regFunc : holidayRegisterFuncs) {
+ regFunc();
+ }
+}
+
+struct Holiday {
+ Holiday(std::function drawFunc, std::function registerFunc) {
+ holidayDrawFuncs.push_back(drawFunc);
+ holidayRegisterFuncs.push_back(registerFunc);
+ }
+};
+
+#endif //HOLIDAY_HPP
diff --git a/soh/soh/Enhancements/Holiday/ItsHeckinPat.cpp b/soh/soh/Enhancements/Holiday/ItsHeckinPat.cpp
new file mode 100644
index 000000000..778b260ec
--- /dev/null
+++ b/soh/soh/Enhancements/Holiday/ItsHeckinPat.cpp
@@ -0,0 +1,150 @@
+#include "Holiday.hpp"
+#include "soh_assets.h"
+#include "soh/Enhancements/randomizer/3drando/random.hpp"
+#include "soh/frame_interpolation.h"
+#include "soh/Notification/Notification.h"
+#include "objects/gameplay_field_keep/gameplay_field_keep.h"
+#include "soh/Enhancements/custom-message/CustomMessageManager.h"
+#include "soh/util.h"
+#include "soh/Enhancements/randomizer/randomizer.h"
+
+extern "C" {
+#include "macros.h"
+#include "functions.h"
+#include "variables.h"
+extern PlayState* gPlayState;
+}
+extern GetItemEntry vanillaQueuedItemEntry;
+
+#define AUTHOR "ItsHeckinPat"
+#define CVAR(v) "gHoliday." AUTHOR "." v
+
+bool spawningPresents = false;
+
+int collectedPresent = 0;
+
+struct Present {
+};
+
+std::unordered_map presents;
+
+void Present_Init(Actor* actor, PlayState* play) {
+ Present present;
+ presents[actor] = present;
+
+ actor->gravity = -1;
+ Actor_MoveForward(actor);
+ actor->shape.rot.y = Random(0, 0xFFFF);
+
+ Actor_UpdateBgCheckInfo(play, actor, 10.0f, 10.0f, 0.0f, 0xFF);
+}
+
+void Present_Update(Actor* actor, PlayState* play) {
+ Present* present = &presents[actor];
+
+ if (actor->xzDistToPlayer < 50.0f && actor->yDistToPlayer < 50.0f) {
+ collectedPresent++;
+ Notification::Emit({
+ .itemIcon = "RG_TRIFORCE_PIECE",
+ .message = "You collected a present!",
+ .messageColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f),
+ });
+ Actor_Kill(actor);
+ }
+}
+
+void Present_Draw(Actor* actor, PlayState* play) {
+ OPEN_DISPS(play->state.gfxCtx);
+
+ Gfx_SetupDL_25Opa(play->state.gfxCtx);
+
+ Matrix_Scale(30.0f, 30.0f, 30.0f, MTXMODE_APPLY);
+ Matrix_Translate(49.20f, 0.0f, -106.60f, MTXMODE_APPLY);
+ gSPMatrix(POLY_OPA_DISP++, Matrix_NewMtx(play->state.gfxCtx, (char*)__FILE__, __LINE__), G_MTX_MODELVIEW | G_MTX_LOAD);
+ gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, 255);
+ gSPDisplayList(POLY_OPA_DISP++, (Gfx*)gXmasDecor100DL);
+
+ CLOSE_DISPS(play->state.gfxCtx);
+}
+
+void Present_Destroy(Actor* actor, PlayState* play) {
+ presents.erase(actor);
+}
+
+static CollisionPoly presentPoly;
+static f32 raycastResult;
+
+static void OnConfigurationChanged() {
+ COND_HOOK(OnSceneSpawnActors, CVarGetInteger(CVAR("GiftsForNPCs"), 0), []() {
+ presents.clear();
+ Vec3f pos;
+ pos.y = 9999.0f;
+ int spawnAttempts = 0;
+ while (spawnAttempts < 50) {
+ if (GET_PLAYER(gPlayState) != nullptr) {
+ pos.x = GET_PLAYER(gPlayState)->actor.world.pos.x;
+ pos.z = GET_PLAYER(gPlayState)->actor.world.pos.z;
+ } else {
+ pos.x = 0;
+ pos.z = 0;
+ }
+ // X/Z anywhere from -1000.0 to +1000.0 from player
+ pos.x += (float)(Random(0, 2000)) - 1000.0f;
+ pos.z += (float)(Random(0, 2000)) - 1000.0f;
+
+ raycastResult = BgCheck_AnyRaycastFloor1(&gPlayState->colCtx, &presentPoly, &pos);
+
+ if (raycastResult > BGCHECK_Y_MIN) {
+ spawningPresents = true;
+ Actor* actor = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_EN_OE2, pos.x, raycastResult, pos.z, 0, 0, 0, 0, false);
+ spawningPresents = false;
+ // break;
+ }
+
+ spawnAttempts++;
+ }
+ });
+
+ COND_ID_HOOK(ShouldActorInit, ACTOR_EN_OE2, CVarGetInteger(CVAR("GiftsForNPCs"), 0), [](void* actorRef, bool* should) {
+ Actor* actor = (Actor*)actorRef;
+ if (spawningPresents) {
+ actor->init = Present_Init;
+ actor->update = Present_Update;
+ actor->draw = Present_Draw;
+ actor->destroy = Present_Destroy;
+ }
+ });
+
+ COND_ID_HOOK(OnOpenText, 0x1019, CVarGetInteger(CVAR("GiftsForNPCs"), 0), [](u16 * textId, bool* loadFromMessageTable) {
+ if (collectedPresent <= 0) {
+ return;
+ }
+
+ auto messageEntry = CustomMessage("A present??? FOR ME???");
+ messageEntry.Format();
+ messageEntry.LoadIntoFont();
+ *loadFromMessageTable = false;
+
+ vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_PIECE_OF_HEART).GetGIEntry_Copy();
+
+ collectedPresent--;
+ });
+}
+
+static void DrawMenu() {
+ ImGui::SeparatorText(AUTHOR);
+ if (UIWidgets::EnhancementCheckbox("Gifts for NPCs", CVAR("GiftsForNPCs"))) {
+ OnConfigurationChanged();
+ }
+}
+
+static void RegisterMod() {
+ // #region Leave this alone unless you know what you are doing
+ OnConfigurationChanged();
+ // #endregion
+
+ // TODO: Anything you want to run once on startup
+}
+
+// TODO: Uncomment this line to enable the mod
+static Holiday holiday(DrawMenu, RegisterMod);
diff --git a/soh/soh/Enhancements/Holiday/ProxySaw.cpp b/soh/soh/Enhancements/Holiday/ProxySaw.cpp
new file mode 100644
index 000000000..988ae28a9
--- /dev/null
+++ b/soh/soh/Enhancements/Holiday/ProxySaw.cpp
@@ -0,0 +1,231 @@
+#include "Holiday.hpp"
+#include
+#include "soh/UIWidgets.hpp"
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "objects/object_dog/object_dog.h"
+#include "soh/frame_interpolation.h"
+#include "soh/Enhancements/randomizer/3drando/random.hpp"
+#include "soh/Enhancements/randomizer/3drando/location_access.hpp"
+#include "soh/Enhancements/randomizer/entrance.h"
+
+#include "objects/gameplay_field_keep/gameplay_field_keep.h"
+#include "objects/object_md/object_md.h"
+#include "src/overlays/actors/ovl_Door_Ana/z_door_ana.h"
+extern "C" {
+#include "macros.h"
+#include "functions.h"
+#include "variables.h"
+
+extern PlayState* gPlayState;
+extern "C" s16 gEnSnowballId;
+void DoorAna_SetupAction(DoorAna* doorAna, DoorAnaActionFunc actionFunc);
+void DoorAna_GrabPlayer(DoorAna* doorAna, PlayState* play);
+}
+
+#define AUTHOR "ProxySaw"
+#define CVAR(v) "gHoliday." AUTHOR "." v
+
+static CollisionPoly snowballPoly;
+static Vec3f snowballPos;
+static f32 raycastResult;
+
+static u32 iceBlockParams[] = {
+ 0x214,
+ 0x1,
+ 0x11,
+ 0x10,
+ 0x20,
+};
+
+static void SpawnSnowballs() {
+ if (gPlayState->sceneNum != SCENE_HYRULE_FIELD && gPlayState->sceneNum != SCENE_KAKARIKO_VILLAGE) {
+ return;
+ }
+
+ int actorsSpawned = 0;
+
+ while (actorsSpawned < 30) {
+ snowballPos.x = (float)(Random(
+ (gPlayState->sceneNum == SCENE_HYRULE_FIELD ? -10000 : -2700) + 10000,
+ (gPlayState->sceneNum == SCENE_HYRULE_FIELD ? 5000 : 2000) + 10000
+ ) - (float)10000.0f);
+ snowballPos.y = 5000;
+ snowballPos.z = (float)(Random(
+ (gPlayState->sceneNum == SCENE_HYRULE_FIELD ? -1000 : -2000) + 10000,
+ (gPlayState->sceneNum == SCENE_HYRULE_FIELD ? 15000 : 2000) + 10000
+ ) - (float)10000.0f);
+
+ raycastResult = BgCheck_AnyRaycastFloor1(&gPlayState->colCtx, &snowballPoly, &snowballPos);
+
+ if (raycastResult > BGCHECK_Y_MIN) {
+ Actor_Spawn(&gPlayState->actorCtx, gPlayState, gEnSnowballId, snowballPos.x, raycastResult,
+ snowballPos.z, 0, 0, 0, gPlayState->sceneNum == SCENE_HYRULE_FIELD, 0);
+ actorsSpawned++;
+ }
+ }
+}
+
+static void SpawnIcebergs() {
+ if (gPlayState->sceneNum != SCENE_LAKE_HYLIA) {
+ return;
+ }
+
+ int actorsSpawned = 0;
+
+ Vec3f spawnedIceBlockPos[15];
+
+ while (actorsSpawned < 15) {
+ Vec3f iceBlockPos;
+ iceBlockPos.x = (float)(Random(
+ (-4200) + 10000,
+ (3000) + 10000
+ ) - (float)10000.0f);
+ iceBlockPos.y = -1713.0f;
+ iceBlockPos.z = (float)(Random(
+ (2600) + 10000,
+ (9000) + 10000
+ ) - (float)10000.0f);
+
+ raycastResult = BgCheck_AnyRaycastFloor1(&gPlayState->colCtx, &snowballPoly, &iceBlockPos);
+
+ if (raycastResult > BGCHECK_Y_MIN) {
+
+ bool overlaps = false;
+ for (int i = 0; i < actorsSpawned; i++) {
+ if (Math_Vec3f_DistXZ(&spawnedIceBlockPos[i], &iceBlockPos) < 500.0f) {
+ overlaps = true;
+ break;
+ }
+ }
+
+ if (overlaps) {
+ continue;
+ }
+
+ if (LINK_IS_ADULT && !Flags_GetEventChkInf(EVENTCHKINF_RAISED_LAKE_HYLIA_WATER)) {
+ iceBlockPos.y = raycastResult;
+ } else {
+ iceBlockPos.y = -1310.0f;
+ }
+
+ Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_BG_SPOT08_ICEBLOCK, iceBlockPos.x, iceBlockPos.y,
+ iceBlockPos.z, 0, (s16)Random(0, 0xFFFF), 0, RandomElement(iceBlockParams), 0);
+ spawnedIceBlockPos[actorsSpawned] = iceBlockPos;
+ actorsSpawned++;
+ }
+ }
+}
+
+const s16 entrances[] = {
+ 0x0000, 0x0209, 0x0004, 0x0242, 0x0028, 0x0221, 0x0169, 0x0215, 0x0165, 0x024A, 0x0010, 0x021D, 0x0082, 0x01E1, 0x0037, 0x0205,
+ 0x0098, 0x02A6, 0x0088, 0x03D4, 0x0008, 0x03A8, 0x0467, 0x023D, 0x0433, 0x0443, 0x0437, 0x0447, 0x009C, 0x033C, 0x00C9, 0x026A,
+ 0x00C1, 0x0266, 0x0043, 0x03CC, 0x045F, 0x0309, 0x03A0, 0x03D0, 0x007E, 0x026E, 0x0530, 0x01D1, 0x0507, 0x03BC, 0x0388, 0x02A2,
+ 0x0063, 0x01D5, 0x0528, 0x03C0, 0x043B, 0x0067, 0x02FD, 0x0349, 0x0550, 0x04EE, 0x039C, 0x0345, 0x05C8, 0x05DC, 0x0072, 0x034D,
+ 0x030D, 0x0355, 0x037C, 0x03FC, 0x0380, 0x03C4, 0x004F, 0x0378, 0x02F9, 0x042F, 0x05D0, 0x05D4, 0x052C, 0x03B8, 0x016D, 0x01CD,
+ 0x00B7, 0x0201, 0x003B, 0x0463, 0x0588, 0x057C, 0x0578, 0x0340, 0x04C2, 0x03E8, 0x04BE, 0x0482, 0x0315, 0x045B, 0x0371, 0x0394,
+ 0x0272, 0x0211, 0x0053, 0x0472, 0x0453, 0x0351, 0x0384, 0x044B, 0x03EC, 0x04FF, 0x0700, 0x0800, 0x0701, 0x0801, 0x0702, 0x0802,
+ 0x0703, 0x0803, 0x0704, 0x0804, 0x0705, 0x0805, 0x0706, 0x0806, 0x0707, 0x0807, 0x0708, 0x0808, 0x0709, 0x0809, 0x070A, 0x080A,
+ 0x070B, 0x080B, 0x070C, 0x080C, 0x070D, 0x080D, 0x070E, 0x080E, 0x070F, 0x080F, 0x0710, 0x0711, 0x0811, 0x0712, 0x0812,
+ 0x0713, 0x0813, 0x0714, 0x0814, 0x0715, 0x0815, 0x0716, 0x0816, 0x0717, 0x0817, 0x0718, 0x0818, 0x0719, 0x0819, 0x081A,
+ 0x071B, 0x081B, 0x071C, 0x081C, 0x071D, 0x081D, 0x071E, 0x081E, 0x071F, 0x081F, 0x0720, 0x0820, 0x004B, 0x035D, 0x031C, 0x0361,
+ 0x002D, 0x050B, 0x044F, 0x0359, 0x05E0, 0x020D, 0x011E, 0x0286, 0x04E2, 0x04D6, 0x01DD, 0x04DA, 0x00FC, 0x01A9, 0x0185, 0x04DE,
+ 0x0102, 0x0189, 0x0117, 0x018D, 0x0276, 0x01FD, 0x00DB, 0x017D, 0x00EA, 0x0181, 0x0157, 0x01F9, 0x0328, 0x0560, 0x0129, 0x022D,
+ 0x0130, 0x03AC, 0x0123, 0x0365, 0x00B1, 0x0033, 0x0138, 0x025A, 0x0171, 0x025E, 0x00E4, 0x0195, 0x013D, 0x0191, 0x014D, 0x01B9,
+ 0x0246, 0x01C1, 0x0147, 0x01BD, 0x0108, 0x019D, 0x0225, 0x01A1, 0x0219, 0x027E, 0x0554, 0x00BB, 0x0282, 0x0600, 0x04F6, 0x0604,
+ 0x01F1, 0x0568, 0x05F4, 0x040F, 0x0252, 0x040B, 0x00C5, 0x0301, 0x0407, 0x000C, 0x024E, 0x0305, 0x0175, 0x0417, 0x0423, 0x008D,
+ 0x02F5, 0x0413, 0x02B2, 0x0457, 0x047A, 0x010E, 0x0608, 0x0564, 0x060C, 0x0610, 0x0580
+};
+
+static void RandomGrotto_WaitOpen(DoorAna* doorAna, PlayState* play) {
+ Actor* actor = &doorAna->actor;
+ Player* player = GET_PLAYER(play);
+ if (Math_StepToF(&actor->scale.x, 0.01f, 0.001f)) {
+ if ((actor->targetMode != 0) && (play->transitionTrigger == TRANS_TRIGGER_OFF) && (player->stateFlags1 & PLAYER_STATE1_FLOOR_DISABLED) && (player->av1.actionVar1 == 0)) {
+ play->nextEntranceIndex = RandomElement(entrances);
+ DoorAna_SetupAction((DoorAna*)actor, DoorAna_GrabPlayer);
+ } else {
+ if (!Player_InCsMode(play) && !(player->stateFlags1 & (PLAYER_STATE1_ON_HORSE | PLAYER_STATE1_IN_WATER)) &&
+ actor->xzDistToPlayer <= 15.0f && -50.0f <= actor->yDistToPlayer &&
+ actor->yDistToPlayer <= 15.0f) {
+ player->stateFlags1 |= PLAYER_STATE1_FLOOR_DISABLED;
+ actor->targetMode = 1;
+ } else {
+ actor->targetMode = 0;
+ }
+ }
+ }
+ Actor_SetScale(actor, actor->scale.x);
+}
+
+static void SpawnRandomGrotto() {
+ Vec3f pos;
+ pos.y = 9999.0f;
+ int spawnAttempts = 0;
+ while (spawnAttempts < 50) {
+ if (GET_PLAYER(gPlayState) != nullptr) {
+ pos.x = GET_PLAYER(gPlayState)->actor.world.pos.x;
+ pos.z = GET_PLAYER(gPlayState)->actor.world.pos.z;
+ } else {
+ pos.x = 0;
+ pos.z = 0;
+ }
+ // X/Z anywhere from -1000.0 to +1000.0 from player
+ pos.x += (float)(Random(0, 2000)) - 1000.0f;
+ pos.z += (float)(Random(0, 2000)) - 1000.0f;
+
+ raycastResult = BgCheck_AnyRaycastFloor1(&gPlayState->colCtx, &snowballPoly, &pos);
+
+ if (raycastResult > BGCHECK_Y_MIN) {
+ Actor* grotto = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_DOOR_ANA, pos.x, raycastResult, pos.z, 0, 0, 0, 0, false);
+ DoorAna_SetupAction((DoorAna*)grotto, RandomGrotto_WaitOpen);
+ break;
+ }
+
+ spawnAttempts++;
+ }
+
+}
+
+static void ConfigurationChanged() {
+ COND_HOOK(OnSceneSpawnActors, CVarGetInteger(CVAR("Snowballs"), 0), SpawnSnowballs);
+ COND_HOOK(OnPlayerUpdate, CVarGetInteger(CVAR("SuperBonk"), 0), []() {
+ Player* player = GET_PLAYER(gPlayState);
+ if (player->actor.bgCheckFlags & 0x08 && ABS(player->linearVelocity) > 15.0f) {
+ player->yaw = ((player->actor.wallYaw - player->yaw) + player->actor.wallYaw) - 0x8000;
+ Player_PlaySfx(&player->actor, NA_SE_PL_BODY_HIT);
+ }
+ });
+
+ COND_HOOK(OnPlayerBonk, CVarGetInteger(CVAR("SuperBonk"), 0), []() {
+ Player* player = GET_PLAYER(gPlayState);
+
+ player->linearVelocity = -100.0f;
+ });
+ COND_HOOK(OnSceneSpawnActors, CVarGetInteger(CVAR("Icebergs"), 0), SpawnIcebergs);
+ COND_HOOK(OnSceneSpawnActors, CVarGetInteger(CVAR("DownTheRabbitHole"), 0), SpawnRandomGrotto);
+}
+
+static void DrawMenu() {
+ ImGui::SeparatorText(AUTHOR);
+
+ if (UIWidgets::EnhancementCheckbox("Snowballs", CVAR("Snowballs"))) {
+ ConfigurationChanged();
+ }
+ if (UIWidgets::EnhancementCheckbox("Lake Hylia Icebergs", CVAR("Icebergs"))) {
+ ConfigurationChanged();
+ }
+ if (UIWidgets::EnhancementCheckbox("Down the Rabbit Hole", CVAR("DownTheRabbitHole"))) {
+ ConfigurationChanged();
+ }
+ if (UIWidgets::EnhancementCheckbox("Super Bonk", CVAR("SuperBonk"))) {
+ ConfigurationChanged();
+ }
+}
+
+static void RegisterMod() {
+ // #region Leave this alone unless you know what you are doing
+ ConfigurationChanged();
+ // #endregion
+}
+
+static Holiday holiday(DrawMenu, RegisterMod);
diff --git a/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp b/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp
index c24b83fb6..477cb765e 100644
--- a/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp
+++ b/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp
@@ -7,6 +7,12 @@
#include
#include
+#include "soh/util.h"
+
+extern "C" {
+ PlayState* gPlayState;
+}
+
using namespace std::literals::string_literals;
static const std::unordered_map textBoxSpecialCharacters = {
@@ -177,6 +183,15 @@ const TextBoxPosition& CustomMessage::GetTextBoxPosition() const {
return position;
}
+void CustomMessage::LoadIntoFont() {
+ MessageContext* msgCtx = &gPlayState->msgCtx;
+ Font* font = &msgCtx->font;
+ char* buffer = font->msgBuf;
+ const int maxBufferSize = sizeof(font->msgBuf);
+ font->charTexBuf[0] = (type << 4) | position;
+ msgCtx->msgLength = font->msgLength = SohUtils::CopyStringToCharBuffer(GetEnglish(MF_RAW), buffer, maxBufferSize);
+}
+
CustomMessage CustomMessage::operator+(const CustomMessage& right) const {
std::vector newColors = colors;
std::vector rColors = right.GetColors();
diff --git a/soh/soh/Enhancements/custom-message/CustomMessageManager.h b/soh/soh/Enhancements/custom-message/CustomMessageManager.h
index 130feda73..ef9ecd725 100644
--- a/soh/soh/Enhancements/custom-message/CustomMessageManager.h
+++ b/soh/soh/Enhancements/custom-message/CustomMessageManager.h
@@ -69,6 +69,9 @@ class CustomMessage {
void SetTextBoxType(TextBoxType boxType);
const TextBoxPosition& GetTextBoxPosition() const;
+ // To only be used with OnOpenText hook
+ void LoadIntoFont();
+
CustomMessage operator+(const CustomMessage& right) const;
CustomMessage operator+(const std::string& right) const;
void operator+=(const std::string& right);
diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor.h b/soh/soh/Enhancements/game-interactor/GameInteractor.h
index 1667cda00..1ffea910e 100644
--- a/soh/soh/Enhancements/game-interactor/GameInteractor.h
+++ b/soh/soh/Enhancements/game-interactor/GameInteractor.h
@@ -571,6 +571,33 @@ struct HookInfo {
body; \
va_end(args); \
})
+#define COND_HOOK(hookType, condition, body) \
+ { \
+ static HOOK_ID hookId = 0; \
+ GameInteractor::Instance->UnregisterGameHook(hookId); \
+ hookId = 0; \
+ if (condition) { \
+ hookId = GameInteractor::Instance->RegisterGameHook(body); \
+ } \
+ }
+#define COND_ID_HOOK(hookType, id, condition, body) \
+ { \
+ static HOOK_ID hookId = 0; \
+ GameInteractor::Instance->UnregisterGameHookForID(hookId); \
+ hookId = 0; \
+ if (condition) { \
+ hookId = GameInteractor::Instance->RegisterGameHookForID(id, body); \
+ } \
+ }
+#define COND_VB_SHOULD(id, condition, body) \
+ { \
+ static HOOK_ID hookId = 0; \
+ GameInteractor::Instance->UnregisterGameHookForID(hookId); \
+ hookId = 0; \
+ if (condition) { \
+ hookId = REGISTER_VB_SHOULD(id, body); \
+ } \
+ }
class GameInteractor {
public:
diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h
index a2d5c56ec..06ef7cc21 100644
--- a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h
+++ b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h
@@ -20,6 +20,7 @@ DEFINE_HOOK(OnSceneSpawnActors, ());
DEFINE_HOOK(OnPlayerUpdate, ());
DEFINE_HOOK(OnOcarinaSongAction, ());
DEFINE_HOOK(OnShopSlotChange, (uint8_t cursorIndex, int16_t price));
+DEFINE_HOOK(ShouldActorInit, (void* actor, bool* result));
DEFINE_HOOK(OnActorInit, (void* actor));
DEFINE_HOOK(OnActorUpdate, (void* actor));
DEFINE_HOOK(OnActorKill, (void* actor));
@@ -31,6 +32,7 @@ DEFINE_HOOK(OnPlayerHealthChange, (int16_t amount));
DEFINE_HOOK(OnPlayerBottleUpdate, (int16_t contents));
DEFINE_HOOK(OnPlayDestroy, ());
DEFINE_HOOK(OnPlayDrawEnd, ());
+DEFINE_HOOK(OnOpenText, (u16 * textId, bool* loadFromMessageTable));
DEFINE_HOOK(OnVanillaBehavior, (GIVanillaBehavior flag, bool* result, va_list originalArgs));
DEFINE_HOOK(OnSaveFile, (int32_t fileNum));
DEFINE_HOOK(OnLoadFile, (int32_t fileNum));
diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp
index 39fc298a8..a2235f7f0 100644
--- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp
+++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp
@@ -79,6 +79,15 @@ void GameInteractor_ExecuteOnActorInit(void* actor) {
GameInteractor::Instance->ExecuteHooksForFilter(actor);
}
+bool GameInteractor_ShouldActorInit(void* actor) {
+ bool result = true;
+ GameInteractor::Instance->ExecuteHooks(actor, &result);
+ GameInteractor::Instance->ExecuteHooksForID(((Actor*)actor)->id, actor, &result);
+ GameInteractor::Instance->ExecuteHooksForPtr((uintptr_t)actor, actor, &result);
+ GameInteractor::Instance->ExecuteHooksForFilter(actor, &result);
+ return result;
+}
+
void GameInteractor_ExecuteOnActorUpdate(void* actor) {
GameInteractor::Instance->ExecuteHooks(actor);
GameInteractor::Instance->ExecuteHooksForID(((Actor*)actor)->id, actor);
@@ -131,6 +140,12 @@ void GameInteractor_ExecuteOnPlayDrawEnd() {
GameInteractor::Instance->ExecuteHooks();
}
+void GameInteractor_ExecuteOnOpenText(u16* textId, bool* loadFromMessageTable) {
+ GameInteractor::Instance->ExecuteHooks(textId, loadFromMessageTable);
+ GameInteractor::Instance->ExecuteHooksForID(*textId, textId, loadFromMessageTable);
+ GameInteractor::Instance->ExecuteHooksForFilter(textId, loadFromMessageTable);
+}
+
bool GameInteractor_Should(GIVanillaBehavior flag, u32 result, ...) {
// Only the external function can use the Variadic Function syntax
// To pass the va args to the next caller must be done using va_list and reading the args into it
diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h
index 3438d269d..60326614d 100644
--- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h
+++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h
@@ -19,6 +19,7 @@ void GameInteractor_ExecuteOnFlagUnset(int16_t flagType, int16_t flag);
void GameInteractor_ExecuteOnSceneSpawnActors();
void GameInteractor_ExecuteOnPlayerUpdate();
void GameInteractor_ExecuteOnOcarinaSongAction();
+bool GameInteractor_ShouldActorInit(void* actor);
void GameInteractor_ExecuteOnActorInit(void* actor);
void GameInteractor_ExecuteOnActorUpdate(void* actor);
void GameInteractor_ExecuteOnActorKill(void* actor);
@@ -32,6 +33,7 @@ void GameInteractor_ExecuteOnOcarinaSongAction();
void GameInteractor_ExecuteOnShopSlotChangeHooks(uint8_t cursorIndex, int16_t price);
void GameInteractor_ExecuteOnPlayDestroy();
void GameInteractor_ExecuteOnPlayDrawEnd();
+void GameInteractor_ExecuteOnOpenText(u16* textId, bool* loadFromMessageTable);
bool GameInteractor_Should(GIVanillaBehavior flag, uint32_t result, ...);
// MARK: - Save Files
diff --git a/soh/soh/Enhancements/mods.cpp b/soh/soh/Enhancements/mods.cpp
index 5059d9b83..2e943b37f 100644
--- a/soh/soh/Enhancements/mods.cpp
+++ b/soh/soh/Enhancements/mods.cpp
@@ -13,6 +13,7 @@
#include "soh/Enhancements/TimeSavers/TimeSavers.h"
#include "soh/Enhancements/cheat_hook_handlers.h"
#include "soh/Enhancements/randomizer/hook_handlers.h"
+#include "soh/Enhancements/Holiday/Holiday.hpp"
#include "objects/object_gi_compass/object_gi_compass.h"
#include "src/overlays/actors/ovl_En_Bb/z_en_bb.h"
@@ -1413,99 +1414,6 @@ void RegisterRandomizerCompasses() {
});
}
-static CollisionPoly snowballPoly;
-static Vec3f snowballPos;
-static f32 raycastResult;
-
-static u32 iceBlockParams[] = {
- 0x214,
- 0x1,
- 0x11,
- 0x10,
- 0x20,
-};
-
-void RegisterSnowballs() {
- GameInteractor::Instance->RegisterGameHook([]() {
- if (gPlayState->sceneNum != SCENE_HYRULE_FIELD && gPlayState->sceneNum != SCENE_KAKARIKO_VILLAGE) {
- return;
- }
-
- int actorsSpawned = 0;
-
- while (actorsSpawned < 30) {
- snowballPos.x = (float)(Random(
- (gPlayState->sceneNum == SCENE_HYRULE_FIELD ? -10000 : -2700) + 10000,
- (gPlayState->sceneNum == SCENE_HYRULE_FIELD ? 5000 : 2000) + 10000
- ) - (float)10000.0f);
- snowballPos.y = 5000;
- snowballPos.z = (float)(Random(
- (gPlayState->sceneNum == SCENE_HYRULE_FIELD ? -1000 : -2000) + 10000,
- (gPlayState->sceneNum == SCENE_HYRULE_FIELD ? 15000 : 2000) + 10000
- ) - (float)10000.0f);
-
- raycastResult = BgCheck_AnyRaycastFloor1(&gPlayState->colCtx, &snowballPoly, &snowballPos);
-
- if (raycastResult > BGCHECK_Y_MIN) {
- Actor_Spawn(&gPlayState->actorCtx, gPlayState, gEnSnowballId, snowballPos.x, raycastResult,
- snowballPos.z, 0, 0, 0, gPlayState->sceneNum == SCENE_HYRULE_FIELD, 0);
- actorsSpawned++;
- }
- }
- });
-
- GameInteractor::Instance->RegisterGameHook([]() {
- if (gPlayState->sceneNum != SCENE_LAKE_HYLIA) {
- return;
- }
-
- int actorsSpawned = 0;
-
- Vec3f spawnedIceBlockPos[15];
-
- while (actorsSpawned < 15) {
- Vec3f iceBlockPos;
- iceBlockPos.x = (float)(Random(
- (-4200) + 10000,
- (3000) + 10000
- ) - (float)10000.0f);
- iceBlockPos.y = -1713.0f;
- iceBlockPos.z = (float)(Random(
- (2600) + 10000,
- (9000) + 10000
- ) - (float)10000.0f);
-
- raycastResult = BgCheck_AnyRaycastFloor1(&gPlayState->colCtx, &snowballPoly, &iceBlockPos);
-
- if (raycastResult > BGCHECK_Y_MIN) {
-
- bool overlaps = false;
- for (int i = 0; i < actorsSpawned; i++) {
- if (Math_Vec3f_DistXZ(&spawnedIceBlockPos[i], &iceBlockPos) < 500.0f) {
- overlaps = true;
- break;
- }
- }
-
- if (overlaps) {
- continue;
- }
-
- if (LINK_IS_ADULT && !Flags_GetEventChkInf(EVENTCHKINF_RAISED_LAKE_HYLIA_WATER)) {
- iceBlockPos.y = raycastResult;
- } else {
- iceBlockPos.y = -1310.0f;
- }
-
- Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_BG_SPOT08_ICEBLOCK, iceBlockPos.x, iceBlockPos.y,
- iceBlockPos.z, 0, (s16)Random(0, 0xFFFF), 0, RandomElement(iceBlockParams), 0);
- spawnedIceBlockPos[actorsSpawned] = iceBlockPos;
- actorsSpawned++;
- }
- }
- });
-}
-
void InitMods() {
BossRush_RegisterHooks();
RandomizerRegisterHooks();
@@ -1544,11 +1452,11 @@ void InitMods() {
RegisterOpenAllHours();
RegisterToTMedallions();
RegisterRandomizerCompasses();
- RegisterSnowballs();
NameTag_RegisterHooks();
RegisterFloorSwitchesHook();
RegisterPatchHandHandler();
RegisterHurtContainerModeHandler();
RegisterPauseMenuHooks();
RandoKaleido_RegisterHooks();
+ RegisterHoliday();
}
diff --git a/soh/soh/Enhancements/timesaver_hook_handlers.cpp b/soh/soh/Enhancements/timesaver_hook_handlers.cpp
index 3e1bc9de1..e5ddf73e7 100644
--- a/soh/soh/Enhancements/timesaver_hook_handlers.cpp
+++ b/soh/soh/Enhancements/timesaver_hook_handlers.cpp
@@ -892,7 +892,7 @@ void TimeSaverOnSceneInitHandler(int16_t sceneNum) {
}
}
-static GetItemEntry vanillaQueuedItemEntry = GET_ITEM_NONE;
+GetItemEntry vanillaQueuedItemEntry = GET_ITEM_NONE;
void TimeSaverOnFlagSetHandler(int16_t flagType, int16_t flag) {
if (!CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO)) return;
@@ -1063,11 +1063,11 @@ void TimeSaverRegisterHooks() {
onVanillaBehaviorHook = GameInteractor::Instance->RegisterGameHook(TimeSaverOnVanillaBehaviorHandler);
onActorInitHook = GameInteractor::Instance->RegisterGameHook(TimeSaverOnActorInitHandler);
onGameFrameUpdate = GameInteractor::Instance->RegisterGameHook(TimeSaverOnGameFrameUpdateHandler);
+ onPlayerUpdateHook = GameInteractor::Instance->RegisterGameHook(TimeSaverOnPlayerUpdateHandler);
+ onItemReceiveHook = GameInteractor::Instance->RegisterGameHook(TimeSaverOnItemReceiveHandler);
if (IS_RANDO) return;
onFlagSetHook = GameInteractor::Instance->RegisterGameHook(TimeSaverOnFlagSetHandler);
- onPlayerUpdateHook = GameInteractor::Instance->RegisterGameHook(TimeSaverOnPlayerUpdateHandler);
- onItemReceiveHook = GameInteractor::Instance->RegisterGameHook(TimeSaverOnItemReceiveHandler);
});
}
diff --git a/soh/soh/SohMenuBar.cpp b/soh/soh/SohMenuBar.cpp
index bec69cceb..01f54b276 100644
--- a/soh/soh/SohMenuBar.cpp
+++ b/soh/soh/SohMenuBar.cpp
@@ -41,6 +41,7 @@
#include "Enhancements/resolution-editor/ResolutionEditor.h"
#include "Enhancements/enemyrandomizer.h"
#include "Enhancements/timesplits/TimeSplits.h"
+#include "Enhancements/Holiday/Holiday.hpp"
// FA icons are kind of wonky, if they worked how I expected them to the "+ 2.0f" wouldn't be needed, but
// they don't work how I expect them to so I added that because it looked good when I eyeballed it
@@ -2256,6 +2257,10 @@ void SohMenuBar::DrawElement() {
DrawRandomizerMenu();
+ ImGui::SetCursorPosY(0.0f);
+
+ DrawHolidayMenu();
+
ImGui::PopStyleVar(1);
ImGui::EndMenuBar();
}
diff --git a/soh/soh/util.cpp b/soh/soh/util.cpp
index faa171231..297e63db3 100644
--- a/soh/soh/util.cpp
+++ b/soh/soh/util.cpp
@@ -391,6 +391,19 @@ size_t SohUtils::CopyStringToCharBuffer(char* buffer, const std::string& source,
return 0;
}
+int SohUtils::CopyStringToCharBuffer(const std::string& inputStr, char* buffer, const int maxBufferSize) {
+ if (!inputStr.empty()) {
+ // Prevent potential horrible overflow due to implicit conversion of maxBufferSize to an unsigned. Prevents negatives.
+ memset(buffer, 0, std::max(0, maxBufferSize));
+ // Gaurentee that this value will be greater than 0, regardless of passed variables.
+ const int copiedCharLen = std::min(std::max(0, maxBufferSize - 1), inputStr.length());
+ memcpy(buffer, inputStr.c_str(), copiedCharLen);
+ return copiedCharLen;
+ }
+
+ return 0;
+}
+
bool SohUtils::IsStringEmpty(std::string str) {
// Remove spaces at the beginning of the string
std::string::size_type start = str.find_first_not_of(' ');
diff --git a/soh/soh/util.h b/soh/soh/util.h
index a37279a18..a566a5382 100644
--- a/soh/soh/util.h
+++ b/soh/soh/util.h
@@ -20,6 +20,7 @@ namespace SohUtils {
// Copies a string into a char buffer up to maxBufferSize characters. This does NOT insert a null terminator
// on the end, as this is used for in-game messages which are not null-terminated.
size_t CopyStringToCharBuffer(char* buffer, const std::string& source, size_t maxBufferSize);
+ int CopyStringToCharBuffer(const std::string& inputStr, char* buffer, const int maxBufferSize);
bool IsStringEmpty(std::string str);
} // namespace SohUtils
diff --git a/soh/src/code/z_actor.c b/soh/src/code/z_actor.c
index dccb4d9bf..165ff4c47 100644
--- a/soh/src/code/z_actor.c
+++ b/soh/src/code/z_actor.c
@@ -1232,14 +1232,19 @@ void Actor_Init(Actor* actor, PlayState* play) {
ActorShape_Init(&actor->shape, 0.0f, NULL, 0.0f);
if (Object_IsLoaded(&play->objectCtx, actor->objBankIndex)) {
Actor_SetObjectDependency(play, actor);
- actor->init(actor, play);
- actor->init = NULL;
+ if (GameInteractor_ShouldActorInit(actor)) {
+ actor->init(actor, play);
+ actor->init = NULL;
- GameInteractor_ExecuteOnActorInit(actor);
+ GameInteractor_ExecuteOnActorInit(actor);
- // For enemy health bar we need to know the max health during init
- if (actor->category == ACTORCAT_ENEMY) {
- actor->maximumHealth = actor->colChkInfo.health;
+ // For enemy health bar we need to know the max health during init
+ if (actor->category == ACTORCAT_ENEMY) {
+ actor->maximumHealth = actor->colChkInfo.health;
+ }
+ } else {
+ actor->init = NULL;
+ Actor_Kill(actor);
}
}
}
@@ -2577,14 +2582,19 @@ void Actor_UpdateAll(PlayState* play, ActorContext* actorCtx) {
if (Object_IsLoaded(&play->objectCtx, actor->objBankIndex))
{
Actor_SetObjectDependency(play, actor);
- actor->init(actor, play);
- actor->init = NULL;
+ if (GameInteractor_ShouldActorInit(actor)) {
+ actor->init(actor, play);
+ actor->init = NULL;
- GameInteractor_ExecuteOnActorInit(actor);
+ GameInteractor_ExecuteOnActorInit(actor);
- // For enemy health bar we need to know the max health during init
- if (actor->category == ACTORCAT_ENEMY) {
- actor->maximumHealth = actor->colChkInfo.health;
+ // For enemy health bar we need to know the max health during init
+ if (actor->category == ACTORCAT_ENEMY) {
+ actor->maximumHealth = actor->colChkInfo.health;
+ }
+ } else {
+ actor->init = NULL;
+ Actor_Kill(actor);
}
}
actor = actor->next;
diff --git a/soh/src/code/z_message_PAL.c b/soh/src/code/z_message_PAL.c
index 53b2bb943..a53626ed9 100644
--- a/soh/src/code/z_message_PAL.c
+++ b/soh/src/code/z_message_PAL.c
@@ -1594,6 +1594,9 @@ void Message_OpenText(PlayState* play, u16 textId) {
Font* font = &msgCtx->font;
s16 textBoxType;
+ bool loadFromMessageTable = true;
+ GameInteractor_ExecuteOnOpenText(&textId, &loadFromMessageTable);
+
if (msgCtx->msgMode == MSGMODE_NONE) {
gSaveContext.unk_13EE = gSaveContext.unk_13EA;
}
@@ -1652,7 +1655,9 @@ void Message_OpenText(PlayState* play, u16 textId) {
}
// RANDOTODO: Use this for ice trap messages
- if (CustomMessage_RetrieveIfExists(play)) {
+ if (!loadFromMessageTable) {
+ // no-op
+ } else if (CustomMessage_RetrieveIfExists(play)) {
osSyncPrintf("Found custom message");
} else if (sTextIsCredits) {
Message_FindCreditsMessage(play, textId);
diff --git a/soh/src/overlays/actors/ovl_Door_Ana/z_door_ana.c b/soh/src/overlays/actors/ovl_Door_Ana/z_door_ana.c
index ef2134f6e..8471b2303 100644
--- a/soh/src/overlays/actors/ovl_Door_Ana/z_door_ana.c
+++ b/soh/src/overlays/actors/ovl_Door_Ana/z_door_ana.c
@@ -179,7 +179,7 @@ void DoorAna_Update(Actor* thisx, PlayState* play) {
this->actionFunc(this, play);
// Changes the grottos facing angle based on camera angle
- if (!CVarGetInteger(CVAR_ENHANCEMENT("DisableGrottoRotation"), 0)) {
+ if (!CVarGetInteger(CVAR_ENHANCEMENT("DisableGrottoRotation"), 0) && thisx->draw == DoorAna_Draw) {
this->actor.shape.rot.y = Camera_GetCamDirYaw(GET_ACTIVE_CAM(play)) + 0x8000;
}
}
diff --git a/soh/src/overlays/actors/ovl_En_Clear_Tag/z_en_clear_tag.c b/soh/src/overlays/actors/ovl_En_Clear_Tag/z_en_clear_tag.c
index c19f43dc3..7b381ef1c 100644
--- a/soh/src/overlays/actors/ovl_En_Clear_Tag/z_en_clear_tag.c
+++ b/soh/src/overlays/actors/ovl_En_Clear_Tag/z_en_clear_tag.c
@@ -244,15 +244,22 @@ void EnClearTag_Init(Actor* thisx, PlayState* play) {
if (this->actor.params == CLEAR_TAG_LASER) {
this->state = CLEAR_TAG_STATE_LASER;
this->timers[CLEAR_TAG_TIMER_LASER_DEATH] = 70;
- this->actor.speedXZ = 35.0f;
- func_8002D908(&this->actor);
- for (j = 0; j <= 0; j++) {
- func_8002D7EC(&this->actor);
+ if (CVarGetInteger("gHoliday.AGreenSpoon.EvilGossipStone", 0)) {
+ this->actor.scale.x = 0.4f;
+ this->actor.scale.y = 0.4f;
+ this->actor.scale.z = 2.0f;
+ this->actor.speedXZ = MAX(10.0f, Actor_WorldDistXZToActor(thisx, &GET_PLAYER(gPlayState)->actor) * 0.33f);
+ } else {
+ this->actor.speedXZ = 35.0f;
+ func_8002D908(&this->actor);
+ for (j = 0; j <= 0; j++) {
+ func_8002D7EC(&this->actor);
+ }
+ this->actor.scale.x = 0.4f;
+ this->actor.scale.y = 0.4f;
+ this->actor.scale.z = 2.0f;
+ this->actor.speedXZ = 70.0f;
}
- this->actor.scale.x = 0.4f;
- this->actor.scale.y = 0.4f;
- this->actor.scale.z = 2.0f;
- this->actor.speedXZ = 70.0f;
this->actor.shape.rot.x = -this->actor.shape.rot.x;
func_8002D908(&this->actor);
@@ -570,12 +577,21 @@ void EnClearTag_Update(Actor* thisx, PlayState* play2) {
}
// Set laser collider properties.
- this->collider.dim.radius = 23;
- this->collider.dim.height = 25;
- this->collider.dim.yShift = -10;
- Collider_UpdateCylinder(&this->actor, &this->collider);
- CollisionCheck_SetAT(play, &play->colChkCtx, &this->collider.base);
- Actor_UpdateBgCheckInfo(play, &this->actor, 50.0f, 80.0f, 100.0f, 5);
+ if (CVarGetInteger("gHoliday.AGreenSpoon.EvilGossipStone", 0)) {
+ this->collider.dim.radius = 10;
+ this->collider.dim.height = 25;
+ this->collider.dim.yShift = -10;
+ Collider_UpdateCylinder(&this->actor, &this->collider);
+ CollisionCheck_SetAT(play, &play->colChkCtx, &this->collider.base);
+ Actor_UpdateBgCheckInfo(play, &this->actor, 10.0f, 10.0f, 10.0f, 5);
+ } else {
+ this->collider.dim.radius = 23;
+ this->collider.dim.height = 25;
+ this->collider.dim.yShift = -10;
+ Collider_UpdateCylinder(&this->actor, &this->collider);
+ CollisionCheck_SetAT(play, &play->colChkCtx, &this->collider.base);
+ Actor_UpdateBgCheckInfo(play, &this->actor, 50.0f, 80.0f, 100.0f, 5);
+ }
// Check if the laser has hit a target, timed out, or hit the ground.
if (this->actor.bgCheckFlags & 9 || hasAtHit || this->timers[CLEAR_TAG_TIMER_LASER_DEATH] == 0) {
diff --git a/soh/src/overlays/actors/ovl_En_Nutsball/z_en_nutsball.c b/soh/src/overlays/actors/ovl_En_Nutsball/z_en_nutsball.c
index 2a37c90f6..2816c50d2 100644
--- a/soh/src/overlays/actors/ovl_En_Nutsball/z_en_nutsball.c
+++ b/soh/src/overlays/actors/ovl_En_Nutsball/z_en_nutsball.c
@@ -78,7 +78,7 @@ void EnNutsball_Init(Actor* thisx, PlayState* play) {
ActorShape_Init(&this->actor.shape, 400.0f, ActorShadow_DrawCircle, 13.0f);
Collider_InitCylinder(play, &this->collider);
Collider_SetCylinder(play, &this->collider, &this->actor, &sCylinderInit);
- if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0)) {
+ if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || CVarGetInteger("gLetItSnow", 0)) {
this->objBankIndex = 0;
} else {
this->objBankIndex = Object_GetIndex(&play->objectCtx, sObjectIDs[this->actor.params]);