From 5965b55eb660de80c81a99d27100b45e3628597f Mon Sep 17 00:00:00 2001
From: Sunk <69110309+Sunketchupm@users.noreply.github.com>
Date: Mon, 25 Nov 2024 04:26:37 -0500
Subject: [PATCH] Custom exclamation boxes (again) (#360)
* Unhardcode exclamation boxes
Part 1 of creating custom exclamation boxes
* Add `set_exclamation_box` and `get_exclamation_box`
Part 2 of creating custom exclamation boxes
* Exclamation box docs
Part 3 of creating exclamation boxes, although this was entirely optional
* Safety check
* Add a warning
* Remake component check
* Update warnings
* Check for subtables not being tables
* Reduce exclamation box size
This matches how many contents could exist in vanilla due to its hardcoded id 99 check. This check allows me to reasonably assume that no romhacks have more than 99 custom contents. Other mods that may use this function really shouldn't need more than 99 contents either.
---
autogen/convert_functions.py | 80 +++++++++++++++++
autogen/lua_definitions/manual.lua | 24 +++++
autogen/lua_definitions/structs.lua | 7 ++
docs/lua/functions.md | 80 +++++++++++++++++
docs/lua/structs.md | 15 ++++
src/game/behavior_actions.c | 10 +--
src/game/behaviors/exclamation_box.inc.c | 43 +++------
src/game/hardcoded.c | 30 +++++++
src/game/hardcoded.h | 15 ++++
src/pc/lua/smlua_cobject_autogen.c | 10 +++
src/pc/lua/smlua_cobject_autogen.h | 1 +
src/pc/lua/smlua_functions.c | 107 +++++++++++++++++++++++
12 files changed, 383 insertions(+), 39 deletions(-)
diff --git a/autogen/convert_functions.py b/autogen/convert_functions.py
index 593d36259..939563cb3 100644
--- a/autogen/convert_functions.py
+++ b/autogen/convert_functions.py
@@ -725,6 +725,86 @@ Shoots a raycast from `startX`, `startY`, and `startZ` in the direction of `dirX
+## [set_exclamation_box_contents](#set_exclamation_box_contents)
+
+Sets the contents that the exclamation box spawns. A single content has 5 keys: `id`, `unused`, `firstByte`, `model`, and `behavior`.
+* `id`: Required; what value the box's oBehParams2ndByte needs to be to spawn this object.
+* `unused`: Optional; unused by vanilla.
+* `firstByte`: Optional; Overrides the 1st byte given to the spawned object.
+* `model`: Required; The model that the object will spawn with. Uses `ModelExtendedId`.
+* `behavior`: Required; The behavior ID that the object will spawn with. Uses `BehaviorId`.
+
+### Lua Example
+```lua
+set_exclamation_box_contents({
+ {id = 0, unused = 0, firstByte = 0, model = E_MODEL_GOOMBA, behavior = id_bhvGoomba}, -- Uses both optional fields
+ {id = 1, unused = 0, model = E_MODEL_KOOPA_WITH_SHELL, behavior = id_bhvKoopa}, -- Only uses `unused` optional field
+ {id = 2, firsteByte = model = E_MODEL_BLACK_BOBOMB, behavior = id_bhvBobomb}, -- Only uses `firstByte` optional field
+ {id = 3, model = E_MODEL_BOO, behavior = id_bhvBoo}, -- Uses no optional fields
+})
+```
+
+### Parameters
+There exists only 1 parameter to this function which is the main table. However, each subtable has 5 different keys that could be accessed.
+| Field | Type |
+| ----- | ---- |
+| id | `integer` |
+| unused (Optional) | `integer` |
+| firstByte (Optional) | `integer` |
+| model | [ModelExtendedId](#ModelExtendedId) |
+| behavior | [BehaviorId](#BehaviorId) |
+
+### Returns
+- None
+
+### C Prototype
+N/A
+
+[:arrow_up_small:](#)
+
+
+
+## [get_exclamation_box_contents](#get_exclamation_box_contents)
+
+Gets the contents that the exclamation box spawns. A single content has 5 keys: `id`, `unused`, `firstByte`, `model`, and `behavior`.
+* `id`: Required; what value the box's oBehParams2ndByte needs to be to spawn this object.
+* `unused`: Optional; unused by vanilla.
+* `firstByte`: Optional; Overrides the 1st byte given to the spawned object.
+* `model`: Required; The model that the object will spawn with. Uses `ModelExtendedId`.
+* `behavior`: Required; The behavior ID that the object will spawn with. Uses `BehaviorId`.
+
+### Lua Example
+```lua
+local contents = get_exclamation_box_contents()
+for index, content in pairs(contents) do -- Enter the main table
+ djui_chat_message_create("Table index " .. index) -- Print the current table index
+ for key, value in pairs(content) do
+ djui_chat_message_create(key .. ": " .. value) -- Print a key-value pair within this subtable
+ end
+ djui_chat_message_create("---------------------------------") -- Separator
+end
+```
+
+### Parameters
+- N/A
+
+### Returns
+The function itself does not return every key/value pair. Instead it returns the main table which holds all the subtables that hold each key/value pair.
+| Field | Type |
+| ----- | ---- |
+| id | `integer` |
+| unused (Optional) | `integer` |
+| firstByte (Optional) | `integer` |
+| model | [ModelExtendedId](#ModelExtendedId) |
+| behavior | [BehaviorId](#BehaviorId) |
+
+### C Prototype
+N/A
+
+[:arrow_up_small:](#)
+
+
+
"""
############################################################################
diff --git a/autogen/lua_definitions/manual.lua b/autogen/lua_definitions/manual.lua
index 5f4478243..4c8063063 100644
--- a/autogen/lua_definitions/manual.lua
+++ b/autogen/lua_definitions/manual.lua
@@ -388,3 +388,27 @@ end
function collision_find_surface_on_ray(startX, startY, startZ, dirX, dirY, dirZ, precision)
-- ...
end
+
+--- @param contents ExclamationBoxContent[]
+--- Sets the contents that the exclamation box spawns.
+--- A single content has 5 keys: `id`, `unused`, `firstByte`, `model`, and `behavior`.
+--- * `id`: Required; what value the box's oBehParams2ndByte needs to be to spawn this object.
+--- * `unused`: Optional; unused by vanilla.
+--- * `firstByte`: Optional; Overrides the 1st byte given to the spawned object.
+--- * `model`: Required; The model that the object will spawn with. Uses `ModelExtendedId`.
+--- * `behavior`: Required; The behavior ID that the object will spawn with. Uses `BehaviorId`.
+function set_exclamation_box_contents(contents)
+ -- ...
+end
+
+--- @return ExclamationBoxContent[]
+--- Gets the contents that the exclamation box spawns.
+--- A single content has 5 keys: `id`, `unused`, `firstByte`, `model`, and `behavior`.
+--- * `id`: Required; what value the box's oBehParams2ndByte needs to be to spawn this object.
+--- * `unused`: Optional; unused by vanilla.
+--- * `firstByte`: Optional; Overrides the 1st byte given to the spawned object.
+--- * `model`: Required; The model that the object will spawn with. Uses `ModelExtendedId`.
+--- * `behavior`: Required; The behavior ID that the object will spawn with. Uses `BehaviorId`.
+function get_exclamation_box_contents()
+ -- ...
+end
diff --git a/autogen/lua_definitions/structs.lua b/autogen/lua_definitions/structs.lua
index b9f0e26a4..d3fd460fb 100644
--- a/autogen/lua_definitions/structs.lua
+++ b/autogen/lua_definitions/structs.lua
@@ -590,6 +590,13 @@
--- @field public g integer
--- @field public r integer
+--- @class ExclamationBoxContent
+--- @field public behavior BehaviorId
+--- @field public firstByte integer
+--- @field public id integer
+--- @field public model ModelExtendedId
+--- @field public unused integer
+
--- @class FirstPersonCamera
--- @field public centerL boolean
--- @field public crouch number
diff --git a/docs/lua/functions.md b/docs/lua/functions.md
index 782bf43c4..9f28fc269 100644
--- a/docs/lua/functions.md
+++ b/docs/lua/functions.md
@@ -2323,6 +2323,86 @@ Shoots a raycast from `startX`, `startY`, and `startZ` in the direction of `dirX
+## [set_exclamation_box_contents](#set_exclamation_box_contents)
+
+Sets the contents that the exclamation box spawns. A single content has 5 keys: `id`, `unused`, `firstByte`, `model`, and `behavior`.
+* `id`: Required; what value the box's oBehParams2ndByte needs to be to spawn this object.
+* `unused`: Optional; unused by vanilla.
+* `firstByte`: Optional; Overrides the 1st byte given to the spawned object.
+* `model`: Required; The model that the object will spawn with. Uses `ModelExtendedId`.
+* `behavior`: Required; The behavior ID that the object will spawn with. Uses `BehaviorId`.
+
+### Lua Example
+```lua
+set_exclamation_box_contents({
+ {id = 0, unused = 0, firstByte = 0, model = E_MODEL_GOOMBA, behavior = id_bhvGoomba}, -- Uses both optional fields
+ {id = 1, unused = 0, model = E_MODEL_KOOPA_WITH_SHELL, behavior = id_bhvKoopa}, -- Only uses `unused` optional field
+ {id = 2, firsteByte = model = E_MODEL_BLACK_BOBOMB, behavior = id_bhvBobomb}, -- Only uses `firstByte` optional field
+ {id = 3, model = E_MODEL_BOO, behavior = id_bhvBoo}, -- Uses no optional fields
+})
+```
+
+### Parameters
+There exists only 1 parameter to this function which is the main table. However, each subtable has 5 different keys that could be accessed.
+| Field | Type |
+| ----- | ---- |
+| id | `integer` |
+| unused (Optional) | `integer` |
+| firstByte (Optional) | `integer` |
+| model | [ModelExtendedId](#ModelExtendedId) |
+| behavior | [BehaviorId](#BehaviorId) |
+
+### Returns
+- None
+
+### C Prototype
+N/A
+
+[:arrow_up_small:](#)
+
+
+
+## [get_exclamation_box_contents](#get_exclamation_box_contents)
+
+Gets the contents that the exclamation box spawns. A single content has 5 keys: `id`, `unused`, `firstByte`, `model`, and `behavior`.
+* `id`: Required; what value the box's oBehParams2ndByte needs to be to spawn this object.
+* `unused`: Optional; unused by vanilla.
+* `firstByte`: Optional; Overrides the 1st byte given to the spawned object.
+* `model`: Required; The model that the object will spawn with. Uses `ModelExtendedId`.
+* `behavior`: Required; The behavior ID that the object will spawn with. Uses `BehaviorId`.
+
+### Lua Example
+```lua
+local contents = get_exclamation_box_contents()
+for index, content in pairs(contents) do -- Enter the main table
+ djui_chat_message_create("Table index " .. index) -- Print the current table index
+ for key, value in pairs(content) do
+ djui_chat_message_create(key .. ": " .. value) -- Print a key-value pair within this subtable
+ end
+ djui_chat_message_create("---------------------------------") -- Separator
+end
+```
+
+### Parameters
+- N/A
+
+### Returns
+The function itself does not return every key/value pair. Instead it returns the main table which holds all the subtables that hold each key/value pair.
+| Field | Type |
+| ----- | ---- |
+| id | `integer` |
+| unused (Optional) | `integer` |
+| firstByte (Optional) | `integer` |
+| model | [ModelExtendedId](#ModelExtendedId) |
+| behavior | [BehaviorId](#BehaviorId) |
+
+### C Prototype
+N/A
+
+[:arrow_up_small:](#)
+
+
+
---
# functions from area.h
diff --git a/docs/lua/structs.md b/docs/lua/structs.md
index f15b1b0ea..112aec3a0 100644
--- a/docs/lua/structs.md
+++ b/docs/lua/structs.md
@@ -24,6 +24,7 @@
- [CutsceneVariable](#CutsceneVariable)
- [DateTime](#DateTime)
- [DjuiColor](#DjuiColor)
+- [ExclamationBoxContent](#ExclamationBoxContent)
- [FirstPersonCamera](#FirstPersonCamera)
- [FloorGeometry](#FloorGeometry)
- [GlobalObjectAnimations](#GlobalObjectAnimations)
@@ -844,6 +845,20 @@
+## [ExclamationBoxContent](#ExclamationBoxContent)
+
+| Field | Type | Access |
+| ----- | ---- | ------ |
+| behavior | [enum BehaviorId](constants.md#enum-BehaviorId) | |
+| firstByte | `integer` | |
+| id | `integer` | |
+| model | [enum ModelExtendedId](constants.md#enum-ModelExtendedId) | |
+| unused | `integer` | |
+
+[:arrow_up_small:](#)
+
+
+
## [FirstPersonCamera](#FirstPersonCamera)
| Field | Type | Access |
diff --git a/src/game/behavior_actions.c b/src/game/behavior_actions.c
index f2d4f60c8..1a9d34b4b 100644
--- a/src/game/behavior_actions.c
+++ b/src/game/behavior_actions.c
@@ -45,9 +45,9 @@
#include "spawn_sound.h"
#include "game/rng_position.h"
#include "rumble_init.h"
-#include "hardcoded.h"
#include "pc/lua/utils/smlua_model_utils.h"
#include "pc/lua/smlua_hooks.h"
+#include "hardcoded.h"
#define o gCurrentObject
@@ -74,14 +74,6 @@ struct Struct8032F698 {
s16 unk4;
};
-struct Struct802C0DF0 {
- u8 unk0;
- u8 unk1;
- u8 unk2;
- u8 model;
- const BehaviorScript *behavior;
-};
-
struct Struct8032F754 {
s32 unk0;
Vec3f unk1;
diff --git a/src/game/behaviors/exclamation_box.inc.c b/src/game/behaviors/exclamation_box.inc.c
index 96d939f08..e206eeb0e 100644
--- a/src/game/behaviors/exclamation_box.inc.c
+++ b/src/game/behaviors/exclamation_box.inc.c
@@ -12,24 +12,6 @@ struct ObjectHitbox sExclamationBoxHitbox = {
/* hurtboxHeight: */ 30,
};
-// hack: if any other sync objects get added here we have to check for them (search for hack in this file)
-struct Struct802C0DF0 sExclamationBoxContents[] = { { 0, 0, 0, MODEL_MARIOS_WING_CAP, bhvWingCap },
- { 1, 0, 0, MODEL_MARIOS_METAL_CAP, bhvMetalCap },
- { 2, 0, 0, MODEL_MARIOS_CAP, bhvVanishCap },
- { 3, 0, 0, MODEL_KOOPA_SHELL, bhvKoopaShell },
- { 4, 0, 0, MODEL_YELLOW_COIN, bhvSingleCoinGetsSpawned },
- { 5, 0, 0, MODEL_NONE, bhvThreeCoinsSpawn },
- { 6, 0, 0, MODEL_NONE, bhvTenCoinsSpawn },
- { 7, 0, 0, MODEL_1UP, bhv1upWalking },
- { 8, 0, 0, MODEL_STAR, bhvSpawnedStar },
- { 9, 0, 0, MODEL_1UP, bhv1upRunningAway },
- { 10, 0, 1, MODEL_STAR, bhvSpawnedStar },
- { 11, 0, 2, MODEL_STAR, bhvSpawnedStar },
- { 12, 0, 3, MODEL_STAR, bhvSpawnedStar },
- { 13, 0, 4, MODEL_STAR, bhvSpawnedStar },
- { 14, 0, 5, MODEL_STAR, bhvSpawnedStar },
- { 99, 0, 0, 0, NULL } };
-
void bhv_rotating_exclamation_box_loop(void) {
if (!o->parentObj || o->parentObj->oAction != 1)
obj_mark_for_deletion(o);
@@ -122,7 +104,8 @@ static s32 exclamation_replace_model(struct MarioState* m, s32 model) {
}
}
-void exclamation_box_spawn_contents(struct Struct802C0DF0 *a0, u8 a1) {
+void exclamation_box_spawn_contents(struct ExclamationBoxContent *content, u8 itemId) {
+ if (content == NULL) { return; }
struct MarioState* marioState = nearest_mario_state_to_object(o);
struct Object* player = marioState ? marioState->marioObj : NULL;
struct Object *spawnedObject = NULL;
@@ -131,11 +114,11 @@ void exclamation_box_spawn_contents(struct Struct802C0DF0 *a0, u8 a1) {
return;
}
- while (a0->unk0 != 99) {
- if (a1 == a0->unk0) {
- s32 model = exclamation_replace_model(marioState, a0->model);
+ for (u8 i = 0; i < gExclamationBoxSize; i++) {
+ if (itemId == content->id) {
+ s32 model = exclamation_replace_model(marioState, smlua_model_util_load(content->model));
- spawnedObject = spawn_object(o, model, a0->behavior);
+ spawnedObject = spawn_object(o, model, get_behavior_from_id(content->behavior));
if (spawnedObject != NULL) {
spawnedObject->oVelY = 20.0f;
spawnedObject->oForwardVel = 3.0f;
@@ -144,15 +127,15 @@ void exclamation_box_spawn_contents(struct Struct802C0DF0 *a0, u8 a1) {
spawnedObject->globalPlayerIndex = player->globalPlayerIndex;
}
}
- o->oBehParams |= a0->unk2 << 24;
- if (a0->model == 122)
- o->oFlags |= 0x4000;
+ o->oBehParams |= content->firstByte << 24;
+ if (content->model == E_MODEL_STAR)
+ o->oFlags |= OBJ_FLAG_PERSISTENT_RESPAWN;
// send non-star spawn events
// stars cant be sent here to due jankiness in oBehParams
- if (a0->behavior != smlua_override_behavior(bhvSpawnedStar) && spawnedObject != NULL) {
+ if (content->behavior != get_id_from_behavior(smlua_override_behavior(bhvSpawnedStar)) && spawnedObject != NULL) {
// hack: if any other sync objects get spawned here we have to check for them
- if (a0->behavior == smlua_override_behavior(bhvKoopaShell)) {
+ if (content->behavior == get_id_from_behavior(smlua_override_behavior(bhvKoopaShell))) {
sync_object_set_id(spawnedObject);
}
struct Object* spawn_objects[] = { spawnedObject };
@@ -161,12 +144,12 @@ void exclamation_box_spawn_contents(struct Struct802C0DF0 *a0, u8 a1) {
}
break;
}
- a0++;
+ content++;
}
}
void exclamation_box_act_4(void) {
- exclamation_box_spawn_contents(sExclamationBoxContents, o->oBehParams2ndByte);
+ exclamation_box_spawn_contents(gExclamationBoxContents, o->oBehParams2ndByte);
spawn_mist_particles_variable(0, 0, 46.0f);
spawn_triangle_break_particles(20, 139, 0.3f, o->oAnimState);
create_sound_spawner(SOUND_GENERAL_BREAK_BOX);
diff --git a/src/game/hardcoded.c b/src/game/hardcoded.c
index c6e2d707c..b07c56be2 100644
--- a/src/game/hardcoded.c
+++ b/src/game/hardcoded.c
@@ -287,6 +287,32 @@ struct BehaviorValues gDefaultBehaviorValues = {
struct BehaviorValues gBehaviorValues = { 0 };
+struct ExclamationBoxContent sDefaultExclamationBoxContents[] = {
+ { 0, 0, 0, E_MODEL_MARIOS_WING_CAP, id_bhvWingCap },
+ { 1, 0, 0, E_MODEL_MARIOS_METAL_CAP, id_bhvMetalCap },
+ { 2, 0, 0, E_MODEL_MARIOS_CAP, id_bhvVanishCap },
+ { 3, 0, 0, E_MODEL_KOOPA_SHELL, id_bhvKoopaShell },
+ { 4, 0, 0, E_MODEL_YELLOW_COIN, id_bhvSingleCoinGetsSpawned },
+ { 5, 0, 0, E_MODEL_NONE, id_bhvThreeCoinsSpawn },
+ { 6, 0, 0, E_MODEL_NONE, id_bhvTenCoinsSpawn },
+ { 7, 0, 0, E_MODEL_1UP, id_bhv1upWalking },
+ { 8, 0, 0, E_MODEL_STAR, id_bhvSpawnedStar },
+ { 9, 0, 0, E_MODEL_1UP, id_bhv1upRunningAway },
+ { 10, 0, 1, E_MODEL_STAR, id_bhvSpawnedStar },
+ { 11, 0, 2, E_MODEL_STAR, id_bhvSpawnedStar },
+ { 12, 0, 3, E_MODEL_STAR, id_bhvSpawnedStar },
+ { 13, 0, 4, E_MODEL_STAR, id_bhvSpawnedStar },
+ { 14, 0, 5, E_MODEL_STAR, id_bhvSpawnedStar }
+};
+
+// Hack: Create 2 arrays: one that is constantly default and one that can be changed.
+
+struct ExclamationBoxContent sDummyContents[EXCLAMATION_BOX_MAX_SIZE];
+
+struct ExclamationBoxContent* gExclamationBoxContents = sDummyContents;
+
+u8 gExclamationBoxSize = 15;
+
//////////////
// Painting //
//////////////
@@ -336,6 +362,10 @@ AT_STARTUP void hardcoded_reset_default_values(void) {
memcpy(&sl_painting, &default_sl_painting, sizeof(struct Painting));
memcpy(&thi_huge_painting, &default_thi_huge_painting, sizeof(struct Painting));
memcpy(&ttm_slide_painting, &default_ttm_slide_painting, sizeof(struct Painting));
+ memcpy(sDummyContents, sDefaultExclamationBoxContents, sizeof(struct ExclamationBoxContent) * 15);
+
+ gExclamationBoxContents = sDummyContents;
+ gExclamationBoxSize = 15;
gPaintingValues = gDefaultPaintingValues;
}
diff --git a/src/game/hardcoded.h b/src/game/hardcoded.h
index 98aec62e2..54de71a01 100644
--- a/src/game/hardcoded.h
+++ b/src/game/hardcoded.h
@@ -5,6 +5,8 @@
#include "dialog_ids.h"
#include "seq_ids.h"
#include "paintings.h"
+#include "pc/lua/utils/smlua_model_utils.h"
+#include "include/behavior_table.h"
////////////
// Levels //
@@ -266,6 +268,19 @@ struct BehaviorValues {
extern struct BehaviorValues gBehaviorValues;
+#define EXCLAMATION_BOX_MAX_SIZE 99
+
+struct ExclamationBoxContent {
+ u8 id;
+ u8 unused;
+ u8 firstByte;
+ enum ModelExtendedId model;
+ enum BehaviorId behavior;
+};
+
+extern struct ExclamationBoxContent* gExclamationBoxContents;
+extern u8 gExclamationBoxSize;
+
//////////////
// Painting //
//////////////
diff --git a/src/pc/lua/smlua_cobject_autogen.c b/src/pc/lua/smlua_cobject_autogen.c
index b2ab63b8c..6ad328248 100644
--- a/src/pc/lua/smlua_cobject_autogen.c
+++ b/src/pc/lua/smlua_cobject_autogen.c
@@ -677,6 +677,15 @@ static struct LuaObjectField sDjuiColorFields[LUA_DJUI_COLOR_FIELD_COUNT] = {
{ "r", LVT_U8, offsetof(struct DjuiColor, r), false, LOT_NONE },
};
+#define LUA_EXCLAMATION_BOX_CONTENT_FIELD_COUNT 5
+static struct LuaObjectField sExclamationBoxContentFields[LUA_EXCLAMATION_BOX_CONTENT_FIELD_COUNT] = {
+ { "behavior", LVT_S32, offsetof(struct ExclamationBoxContent, behavior), false, LOT_NONE },
+ { "firstByte", LVT_U8, offsetof(struct ExclamationBoxContent, firstByte), false, LOT_NONE },
+ { "id", LVT_U8, offsetof(struct ExclamationBoxContent, id), false, LOT_NONE },
+ { "model", LVT_S32, offsetof(struct ExclamationBoxContent, model), false, LOT_NONE },
+ { "unused", LVT_U8, offsetof(struct ExclamationBoxContent, unused), false, LOT_NONE },
+};
+
#define LUA_FIRST_PERSON_CAMERA_FIELD_COUNT 10
static struct LuaObjectField sFirstPersonCameraFields[LUA_FIRST_PERSON_CAMERA_FIELD_COUNT] = {
{ "centerL", LVT_BOOL, offsetof(struct FirstPersonCamera, centerL), false, LOT_NONE },
@@ -2460,6 +2469,7 @@ struct LuaObjectTable sLuaObjectAutogenTable[LOT_AUTOGEN_MAX - LOT_AUTOGEN_MIN]
{ LOT_CUTSCENEVARIABLE, sCutsceneVariableFields, LUA_CUTSCENE_VARIABLE_FIELD_COUNT },
{ LOT_DATETIME, sDateTimeFields, LUA_DATE_TIME_FIELD_COUNT },
{ LOT_DJUICOLOR, sDjuiColorFields, LUA_DJUI_COLOR_FIELD_COUNT },
+ { LOT_EXCLAMATIONBOXCONTENT, sExclamationBoxContentFields, LUA_EXCLAMATION_BOX_CONTENT_FIELD_COUNT },
{ LOT_FIRSTPERSONCAMERA, sFirstPersonCameraFields, LUA_FIRST_PERSON_CAMERA_FIELD_COUNT },
{ LOT_FLOORGEOMETRY, sFloorGeometryFields, LUA_FLOOR_GEOMETRY_FIELD_COUNT },
{ LOT_GLOBALOBJECTANIMATIONS, sGlobalObjectAnimationsFields, LUA_GLOBAL_OBJECT_ANIMATIONS_FIELD_COUNT },
diff --git a/src/pc/lua/smlua_cobject_autogen.h b/src/pc/lua/smlua_cobject_autogen.h
index 0ca897f14..f08674bfa 100644
--- a/src/pc/lua/smlua_cobject_autogen.h
+++ b/src/pc/lua/smlua_cobject_autogen.h
@@ -27,6 +27,7 @@ enum LuaObjectAutogenType {
LOT_CUTSCENEVARIABLE,
LOT_DATETIME,
LOT_DJUICOLOR,
+ LOT_EXCLAMATIONBOXCONTENT,
LOT_FIRSTPERSONCAMERA,
LOT_FLOORGEOMETRY,
LOT_GLOBALOBJECTANIMATIONS,
diff --git a/src/pc/lua/smlua_functions.c b/src/pc/lua/smlua_functions.c
index b722976b1..dc3ea3b9b 100644
--- a/src/pc/lua/smlua_functions.c
+++ b/src/pc/lua/smlua_functions.c
@@ -15,6 +15,7 @@
#include "include/macro_presets.h"
#include "utils/smlua_anim_utils.h"
#include "utils/smlua_collision_utils.h"
+#include "game/hardcoded.h"
bool smlua_functions_valid_param_count(lua_State* L, int expected) {
int top = lua_gettop(L);
@@ -179,6 +180,110 @@ int smlua_func_network_send_to(lua_State* L) {
return 1;
}
+int smlua_func_set_exclamation_box_contents(lua_State* L) {
+ if (!smlua_functions_valid_param_count(L, 1)) { return 0; }
+
+ if (lua_type(L, 1) != LUA_TTABLE) {
+ LOG_LUA_LINE("Invalid type passed to set_exclamation_box(): %u", lua_type(L, -1));
+ return 0;
+ }
+
+ struct ExclamationBoxContent exclamationBoxNewContents[EXCLAMATION_BOX_MAX_SIZE];
+
+ u8 exclamationBoxIndex = 0;
+ lua_pushnil(L); // Initial pop
+ while (lua_next(L, 1)) /* Main table index */ {
+ if (lua_type(L, 3) != LUA_TTABLE) {
+ LOG_LUA_LINE("set_exclamation_box: Subtable is not a table (Subtable %u)", exclamationBoxIndex);
+ return 0;
+ }
+
+ lua_pushnil(L); // Subtable initial pop
+ bool confirm[] = { false, false, false, false, false }; /* id, unused, firstByte, model, behavior */
+ while (lua_next(L, 3)) /* Subtable index */ {
+ // key is index -2, value is index -1
+ const char* key = smlua_to_string(L, -2);
+ if (!gSmLuaConvertSuccess) {
+ LOG_LUA("set_exclamation_box: Failed to convert subtable key");
+ return 0;
+ }
+
+ s32 value = smlua_to_integer(L, -1);
+ if (!gSmLuaConvertSuccess) {
+ LOG_LUA("set_exclamation_box: Failed to convert subtable value");
+ return 0;
+ }
+
+ // Fill fields
+ if (strcmp(key, "id") == 0) { exclamationBoxNewContents[exclamationBoxIndex].id = value; confirm[0] = true; }
+ else if (strcmp(key, "unused") == 0) { exclamationBoxNewContents[exclamationBoxIndex].unused = value; confirm[1] = true; }
+ else if (strcmp(key, "firstByte") == 0) { exclamationBoxNewContents[exclamationBoxIndex].firstByte = value; confirm[2] = true; }
+ else if (strcmp(key, "model") == 0) { exclamationBoxNewContents[exclamationBoxIndex].model = value; confirm[3] = true; }
+ else if (strcmp(key, "behavior") == 0) { exclamationBoxNewContents[exclamationBoxIndex].behavior = value; confirm[4] = true; }
+ else {
+ LOG_LUA_LINE_WARNING("set_exclamation_box: Invalid key passed (Subtable %d)", exclamationBoxIndex);
+ }
+
+ lua_pop(L, 1); // Pop value
+ }
+ // Check if the fields have been filled
+ if (!(confirm[0]) || !(confirm[3]) || !(confirm[4])) {
+ LOG_LUA("set_exclamation_box: A critical component of a content (id, model, or behavior) has not been set (Subtable %d)", exclamationBoxIndex);
+ return 0;
+ }
+ if (!(confirm[1])) { exclamationBoxNewContents[exclamationBoxIndex].unused = 0; }
+ if (!(confirm[2])) { exclamationBoxNewContents[exclamationBoxIndex].firstByte = 0; }
+
+ if (++exclamationBoxIndex == EXCLAMATION_BOX_MAX_SIZE) { // There is an edge case where the 254th element will warn even though it works just fine
+ // Immediately exit if at risk for out of bounds array access.
+ lua_pop(L, 1);
+ LOG_LUA_LINE_WARNING("set_exclamation_box: Too many items have been set for the exclamation box. Some content spawns may be lost.");
+ break;
+ }
+ lua_pop(L, 1); // Pop subtable
+ }
+
+ memcpy(gExclamationBoxContents, exclamationBoxNewContents, sizeof(struct ExclamationBoxContent) * exclamationBoxIndex);
+ gExclamationBoxSize = exclamationBoxIndex;
+
+ return 1;
+}
+
+int smlua_func_get_exclamation_box_contents(lua_State* L) {
+ if (!smlua_functions_valid_param_count(L, 0)) { return 0; }
+
+ lua_newtable(L); // Index 1
+
+ for (u8 i = 0; i < gExclamationBoxSize; i++) {
+ lua_pushinteger(L, i); // Index 2
+ lua_newtable(L); // Index 3
+
+ lua_pushstring(L, "id");
+ lua_pushinteger(L, gExclamationBoxContents[i].id);
+ lua_settable(L, -3);
+
+ lua_pushstring(L, "unused");
+ lua_pushinteger(L, gExclamationBoxContents[i].unused);
+ lua_settable(L, -3);
+
+ lua_pushstring(L, "firstByte");
+ lua_pushinteger(L, gExclamationBoxContents[i].firstByte);
+ lua_settable(L, -3);
+
+ lua_pushstring(L, "model");
+ lua_pushinteger(L, gExclamationBoxContents[i].model);
+ lua_settable(L, -3);
+
+ lua_pushstring(L, "behavior");
+ lua_pushinteger(L, gExclamationBoxContents[i].behavior);
+ lua_settable(L, -3);
+
+ lua_settable(L, 1); // Insert the subtable into the main table
+ }
+
+ return 1;
+}
+
//////////////
// Textures //
//////////////
@@ -855,6 +960,8 @@ void smlua_bind_functions(void) {
smlua_bind_function(L, "reset_level", smlua_func_reset_level);
smlua_bind_function(L, "network_send", smlua_func_network_send);
smlua_bind_function(L, "network_send_to", smlua_func_network_send_to);
+ smlua_bind_function(L, "set_exclamation_box_contents", smlua_func_set_exclamation_box_contents);
+ smlua_bind_function(L, "get_exclamation_box_contents", smlua_func_get_exclamation_box_contents);
smlua_bind_function(L, "get_texture_info", smlua_func_get_texture_info);
smlua_bind_function(L, "djui_hud_render_texture", smlua_func_djui_hud_render_texture);
smlua_bind_function(L, "djui_hud_render_texture_tile", smlua_func_djui_hud_render_texture_tile);