From 4d5040d15cf1bf180f7f863b43bebcad2bd20823 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Wed, 23 Apr 2025 08:29:01 +0200 Subject: [PATCH 01/52] misc: fix build warnings --- src/libtrx/filesystem.c | 20 ++++++++++---------- src/libtrx/game/game_flow/reader.c | 12 ++++++------ src/libtrx/include/libtrx/filesystem.h | 4 ++-- src/tr2/game/items.c | 2 +- src/tr2/game/lara/draw.c | 9 +++++++++ src/tr2/game/level.c | 6 ++++-- 6 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/libtrx/filesystem.c b/src/libtrx/filesystem.c index 906032745..4ce3ab763 100644 --- a/src/libtrx/filesystem.c +++ b/src/libtrx/filesystem.c @@ -283,56 +283,56 @@ MYFILE *File_Open(const char *path, FILE_OPEN_MODE mode) return file; } -void File_ReadData(MYFILE *const file, void *const data, const size_t size) +bool File_ReadData(MYFILE *const file, void *const data, const size_t size) { - fread(data, size, 1, file->fp); + return fread(data, size, 1, file->fp) == 1; } -void File_ReadItems( +bool File_ReadItems( MYFILE *const file, void *data, const size_t count, const size_t item_size) { - fread(data, item_size, count, file->fp); + return fread(data, item_size, count, file->fp) == count; } int8_t File_ReadS8(MYFILE *const file) { int8_t result; - fread(&result, sizeof(result), 1, file->fp); + File_ReadData(file, &result, sizeof(result)); return result; } int16_t File_ReadS16(MYFILE *const file) { int16_t result; - fread(&result, sizeof(result), 1, file->fp); + File_ReadData(file, &result, sizeof(result)); return result; } int32_t File_ReadS32(MYFILE *const file) { int32_t result; - fread(&result, sizeof(result), 1, file->fp); + File_ReadData(file, &result, sizeof(result)); return result; } uint8_t File_ReadU8(MYFILE *const file) { uint8_t result; - fread(&result, sizeof(result), 1, file->fp); + File_ReadData(file, &result, sizeof(result)); return result; } uint16_t File_ReadU16(MYFILE *const file) { uint16_t result; - fread(&result, sizeof(result), 1, file->fp); + File_ReadData(file, &result, sizeof(result)); return result; } uint32_t File_ReadU32(MYFILE *const file) { uint32_t result; - fread(&result, sizeof(result), 1, file->fp); + File_ReadData(file, &result, sizeof(result)); return result; } diff --git a/src/libtrx/game/game_flow/reader.c b/src/libtrx/game/game_flow/reader.c index 26f096e74..77ff36e2e 100644 --- a/src/libtrx/game/game_flow/reader.c +++ b/src/libtrx/game/game_flow/reader.c @@ -108,12 +108,11 @@ static void M_LoadCommonSettings( if (tmp_value != nullptr && tmp_value->type == JSON_TYPE_ARRAY) { const JSON_ARRAY *const tmp_arr = JSON_ValueAsArray(tmp_value); const RGB_F color = { - JSON_ArrayGetDouble(tmp_arr, 0, JSON_INVALID_NUMBER), - JSON_ArrayGetDouble(tmp_arr, 1, JSON_INVALID_NUMBER), - JSON_ArrayGetDouble(tmp_arr, 2, JSON_INVALID_NUMBER), + JSON_ArrayGetDouble(tmp_arr, 0, -1.0), + JSON_ArrayGetDouble(tmp_arr, 1, -1.0), + JSON_ArrayGetDouble(tmp_arr, 2, -1.0), }; - if (color.r != JSON_INVALID_NUMBER && color.g != JSON_INVALID_NUMBER - && color.b != JSON_INVALID_NUMBER) { + if (color.r >= 0.0 && color.g >= 0.0 && color.b >= 0.0) { settings->water_color.is_present = true; settings->water_color.value = (RGB_888) { color.r * 255.0f, @@ -515,7 +514,8 @@ static void M_LoadTitleLevel(JSON_OBJECT *obj, GAME_FLOW *const gf) JSON_OBJECT *title_obj = JSON_ObjectGetObject(obj, "title"); if (title_obj != nullptr) { gf->title_level = Memory_Alloc(sizeof(GF_LEVEL)); - M_LoadLevel(title_obj, gf, gf->title_level, 0, GFL_TITLE); + M_LoadLevel( + title_obj, gf, gf->title_level, 0, (void *)(intptr_t)GFL_TITLE); } } diff --git a/src/libtrx/include/libtrx/filesystem.h b/src/libtrx/include/libtrx/filesystem.h index 20a789db5..584166af6 100644 --- a/src/libtrx/include/libtrx/filesystem.h +++ b/src/libtrx/include/libtrx/filesystem.h @@ -39,8 +39,8 @@ char *File_GuessExtension(const char *path, const char **extensions); MYFILE *File_Open(const char *path, FILE_OPEN_MODE mode); -void File_ReadData(MYFILE *file, void *data, size_t size); -void File_ReadItems(MYFILE *file, void *data, size_t count, size_t item_size); +bool File_ReadData(MYFILE *file, void *data, size_t size); +bool File_ReadItems(MYFILE *file, void *data, size_t count, size_t item_size); int8_t File_ReadS8(MYFILE *file); int16_t File_ReadS16(MYFILE *file); int32_t File_ReadS32(MYFILE *file); diff --git a/src/tr2/game/items.c b/src/tr2/game/items.c index b04adfdce..a66bb7eb3 100644 --- a/src/tr2/game/items.c +++ b/src/tr2/game/items.c @@ -364,7 +364,7 @@ const BOUNDS_16 *Item_GetBoundsAccurate(const ITEM *const item) ANIM_FRAME *Item_GetBestFrame(const ITEM *const item) { ANIM_FRAME *frames[2]; - int32_t rate; + int32_t rate = 0; const int32_t frac = Item_GetFrames(item, frames, &rate); return frames[(frac > rate / 2) ? 1 : 0]; } diff --git a/src/tr2/game/lara/draw.c b/src/tr2/game/lara/draw.c index 60cf7ce56..04fa37486 100644 --- a/src/tr2/game/lara/draw.c +++ b/src/tr2/game/lara/draw.c @@ -473,6 +473,13 @@ void Lara_Draw_I( Matrix_Rot16_ID(mesh_rots_1[LM_UARM_R], mesh_rots_2[LM_UARM_R]); Output_DrawObjectMesh_I(g_Lara.mesh_ptrs[LM_UARM_R], clip); +// NOTE: gcc wrongly complains about mesh_rots_1 possibly being NULL. +// While this is not the case, it's curious how the pistols subtract the +// frame_base from g_Lara.*_arm.frame_num to access the mesh_rots, and the +// rifles do not. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Warray-bounds" + M_DrawBodyPart(LM_LARM_R, bone, mesh_rots_1, mesh_rots_2, clip); M_DrawBodyPart(LM_HAND_R, bone, mesh_rots_1, mesh_rots_2, clip); @@ -486,6 +493,8 @@ void Lara_Draw_I( M_DrawBodyPart(LM_LARM_L, bone, mesh_rots_1, mesh_rots_2, clip); M_DrawBodyPart(LM_HAND_L, bone, mesh_rots_1, mesh_rots_2, clip); +#pragma GCC diagnostic pop + if (g_Lara.right_arm.flash_gun) { *g_MatrixPtr = saved_matrix; Gun_DrawFlash(gun_type, clip); diff --git a/src/tr2/game/level.c b/src/tr2/game/level.c index 6ff60b7d0..2a128e8fe 100644 --- a/src/tr2/game/level.c +++ b/src/tr2/game/level.c @@ -143,22 +143,24 @@ static int32_t M_CompareSampleOffsets(const void *const a, const void *const b) static void M_InitialiseSoundEffects(const char *file_name) { BENCHMARK benchmark = Benchmark_Start(); + LEVEL_INFO *info = nullptr; SAMPLE_ENTRY *entries = nullptr; + if (file_name == nullptr) { file_name = g_GameFlow.settings.sfx_path; } const char *full_path = File_GetFullPath(file_name == nullptr ? DEFAULT_SFX_PATH : file_name); LOG_DEBUG("Loading samples from %s", full_path); + MYFILE *const fp = File_Open(full_path, FILE_OPEN_READ); Memory_FreePointer(&full_path); - if (fp == nullptr) { Shell_ExitSystemFmt("Could not open %s file", file_name); goto finish; } - LEVEL_INFO *const info = Level_GetInfo(); + info = Level_GetInfo(); const int32_t sample_count = info->samples.offset_count; entries = Memory_Alloc(sizeof(SAMPLE_ENTRY) * sample_count); for (int32_t i = 0; i < sample_count; i++) { From b935707b5b3628b1fdf359c870a6a273540516f2 Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Wed, 23 Apr 2025 15:16:39 +0100 Subject: [PATCH 02/52] tr2/option_controls: fix selected layout not saving Resolves #2830. --- docs/tr2/CHANGELOG.md | 1 + src/tr2/game/option/option_controls.c | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index ca00fd874..b76e09e1a 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,4 +1,5 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...develop) - ××××-××-×× +- fixed the selected keyboard/controller layout not being saved (#2830, regression from 1.0) ## [1.0](https://github.com/LostArtefacts/TRX/compare/tr2-0.10...tr2-1.0) - 2025-04-23 - added support for The Golden Mask (#1621) diff --git a/src/tr2/game/option/option_controls.c b/src/tr2/game/option/option_controls.c index d1fcc0392..0d1321378 100644 --- a/src/tr2/game/option/option_controls.c +++ b/src/tr2/game/option/option_controls.c @@ -44,10 +44,11 @@ static void M_HandleLayoutChange(const EVENT *event, void *user_data) const M_PRIV *const p = user_data; switch (p->ui.state.backend) { case INPUT_BACKEND_KEYBOARD: - g_Config.input.keyboard_layout = p->ui.state.active_layout; + g_Config.input.keyboard_layout = p->ui.state.editor_state.active_layout; break; case INPUT_BACKEND_CONTROLLER: - g_Config.input.controller_layout = p->ui.state.active_layout; + g_Config.input.controller_layout = + p->ui.state.editor_state.active_layout; break; default: break; From 4d36177247cd49b1a2fc536bf686037134d4a4cf Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Wed, 23 Apr 2025 15:17:51 +0100 Subject: [PATCH 03/52] tr2/shell: fix PSX FOV option not being applied immediately Resolves #2831. --- docs/tr2/CHANGELOG.md | 1 + src/tr2/game/shell/common.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index b76e09e1a..c01f51ccf 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,5 +1,6 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...develop) - ××××-××-×× - fixed the selected keyboard/controller layout not being saved (#2830, regression from 1.0) +- fixed toggling the PSX FOV option not having an immediate effect (#2831, regression from 1.0) ## [1.0](https://github.com/LostArtefacts/TRX/compare/tr2-0.10...tr2-1.0) - 2025-04-23 - added support for The Golden Mask (#1621) diff --git a/src/tr2/game/shell/common.c b/src/tr2/game/shell/common.c index c3603f996..f418d734b 100644 --- a/src/tr2/game/shell/common.c +++ b/src/tr2/game/shell/common.c @@ -398,7 +398,7 @@ static void M_HandleConfigChange(const EVENT *const event, void *const data) if (CHANGED(window.is_fullscreen) || CHANGED(window.is_maximized) || CHANGED(window.width) || CHANGED(window.height) || CHANGED(rendering.scaler) || CHANGED(rendering.sizer) - || CHANGED(rendering.aspect_mode)) { + || CHANGED(rendering.aspect_mode) || CHANGED(visuals.use_psx_fov)) { LOG_DEBUG("Change in settings detected"); M_SyncToWindow(); M_RefreshRendererViewport(); From 3030d694a5bca17de0a10fbb3dc36ed33ee9c0ca Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:52:23 +0100 Subject: [PATCH 04/52] tr2/shell: reload background image on aspect change Resolves #2832. --- docs/tr2/CHANGELOG.md | 1 + src/libtrx/game/output/background.c | 13 +++++++++++++ src/libtrx/include/libtrx/game/output/background.h | 1 + src/tr2/game/shell/common.c | 4 ++++ 4 files changed, 19 insertions(+) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index c01f51ccf..9d31fe22d 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...develop) - ××××-××-×× - fixed the selected keyboard/controller layout not being saved (#2830, regression from 1.0) - fixed toggling the PSX FOV option not having an immediate effect (#2831, regression from 1.0) +- fixed changing the aspect ratio not updating the current background image (#2832, regression from 1.0) ## [1.0](https://github.com/LostArtefacts/TRX/compare/tr2-0.10...tr2-1.0) - 2025-04-23 - added support for The Golden Mask (#1621) diff --git a/src/libtrx/game/output/background.c b/src/libtrx/game/output/background.c index 855047d01..4a8a680c4 100644 --- a/src/libtrx/game/output/background.c +++ b/src/libtrx/game/output/background.c @@ -2,6 +2,7 @@ #include "debug.h" #include "filesystem.h" +#include "game/output/common.h" #include "game/viewport.h" #include "log.h" #include "memory.h" @@ -198,6 +199,18 @@ bool Output_LoadBackgroundFromFile(const char *const path) return result; } +void Output_ReloadBackgroundImage(void) +{ + if (m_LastPath == nullptr) { + return; + } + + char *prev = Memory_DupStr(m_LastPath); + Output_UnloadBackground(); + Output_LoadBackgroundFromFile(prev); + Memory_FreePointer(&prev); +} + char *Output_GetLastBackgroundPath(void) { return m_LastPath; diff --git a/src/libtrx/include/libtrx/game/output/background.h b/src/libtrx/include/libtrx/game/output/background.h index 444896925..7c028ec32 100644 --- a/src/libtrx/include/libtrx/game/output/background.h +++ b/src/libtrx/include/libtrx/game/output/background.h @@ -3,6 +3,7 @@ #include "../../engine/image.h" bool Output_LoadBackgroundFromFile(const char *path); +void Output_ReloadBackgroundImage(void); extern bool Output_LoadBackgroundFromImage(const IMAGE *image); extern void Output_LoadBackgroundFromObject(void); diff --git a/src/tr2/game/shell/common.c b/src/tr2/game/shell/common.c index f418d734b..f7d00b73d 100644 --- a/src/tr2/game/shell/common.c +++ b/src/tr2/game/shell/common.c @@ -427,6 +427,10 @@ static void M_HandleConfigChange(const EVENT *const event, void *const data) || CHANGED(visuals.water_color.r)) { Output_ApplyLevelSettings(); } + + if (CHANGED(rendering.aspect_mode)) { + Output_ReloadBackgroundImage(); + } } // TODO: refactor the hell out of me From f590c9243cedf0ff1c4bdf6f637c71669f96b8ae Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Wed, 23 Apr 2025 20:10:00 +0100 Subject: [PATCH 05/52] graphic_settings: add scroll wraparound option This makes the scroll wraparound option available in both games' graphics dialogs. TR1's dialog is also updated to allow scrolling until such times as it's moved to the new UI framework. Resolves #2834. --- data/tr1/ship/cfg/TR1X_strings.json5 | 1 + data/tr2/ship/cfg/TR2X_strings.json5 | 1 + docs/tr1/CHANGELOG.md | 1 + docs/tr2/CHANGELOG.md | 1 + .../include/libtrx/game/game_string.def | 1 + src/tr1/game/option/option_graphics.c | 30 +++++++++++++++++++ src/tr2/game/ui/dialogs/graphic_settings.c | 6 ++++ 7 files changed, 41 insertions(+) diff --git a/data/tr1/ship/cfg/TR1X_strings.json5 b/data/tr1/ship/cfg/TR1X_strings.json5 index a429562bc..b8ea28438 100644 --- a/data/tr1/ship/cfg/TR1X_strings.json5 +++ b/data/tr1/ship/cfg/TR1X_strings.json5 @@ -369,6 +369,7 @@ "DETAIL_TITLE": "Graphic Options", "DETAIL_TRAPEZOID_FILTER": "Trapezoid filter", "DETAIL_UI_BAR_SCALE": "UI bar scale", + "DETAIL_UI_SCROLL_WRAPAROUND": "UI scroll wrap", "DETAIL_UI_TEXT_SCALE": "UI text scale", "DETAIL_VSYNC": "VSync", "DETAIL_WATER_COLOR_B": "Water color (B)", diff --git a/data/tr2/ship/cfg/TR2X_strings.json5 b/data/tr2/ship/cfg/TR2X_strings.json5 index 3be3b698f..abc97764d 100644 --- a/data/tr2/ship/cfg/TR2X_strings.json5 +++ b/data/tr2/ship/cfg/TR2X_strings.json5 @@ -496,6 +496,7 @@ "DETAIL_TITLE": "Graphic Options", "DETAIL_TRAPEZOID_FILTER": "Trapezoid filter", "DETAIL_UI_BAR_SCALE": "UI bar scale", + "DETAIL_UI_SCROLL_WRAPAROUND": "UI scroll wrap", "DETAIL_UI_TEXT_SCALE": "UI text scale", "DETAIL_USE_PSX_FOV": "Use PSX FOV", "DETAIL_WATER_COLOR_B": "Water color (B)", diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index 540e34371..c99cbb79c 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -4,6 +4,7 @@ - added support for a hex water color notation (eg. `#80FFFF`) in the game flow file - added support for antitriggers, like TR2+ (#2580) - added support for aspect ratio-specific images (#1840) +- added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) - changed the `draw_distance_min` and `draw_distance_max` to `fog_start` and `fog_end` - changed `Select Detail` dialog title to `Graphic Options` - changed the number of static mesh slots from 50 to 256 (#2734) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 9d31fe22d..bcfd6a666 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,4 +1,5 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...develop) - ××××-××-×× +- added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) - fixed the selected keyboard/controller layout not being saved (#2830, regression from 1.0) - fixed toggling the PSX FOV option not having an immediate effect (#2831, regression from 1.0) - fixed changing the aspect ratio not updating the current background image (#2832, regression from 1.0) diff --git a/src/libtrx/include/libtrx/game/game_string.def b/src/libtrx/include/libtrx/game/game_string.def index 8eecf1491..63a70fe0d 100644 --- a/src/libtrx/include/libtrx/game/game_string.def +++ b/src/libtrx/include/libtrx/game/game_string.def @@ -150,5 +150,6 @@ GS_DEFINE(DETAIL_TRAPEZOID_FILTER, "Trapezoid filter") GS_DEFINE(DETAIL_RENDER_MODE, "Render mode") GS_DEFINE(DETAIL_UI_TEXT_SCALE, "UI text scale") GS_DEFINE(DETAIL_UI_BAR_SCALE, "UI bar scale") +GS_DEFINE(DETAIL_UI_SCROLL_WRAPAROUND, "UI scroll wrap") GS_DEFINE(PAGINATION_NAV, "%d / %d") GS_DEFINE(MISC_EMPTY_SLOT_FMT, "- EMPTY SLOT -") diff --git a/src/tr1/game/option/option_graphics.c b/src/tr1/game/option/option_graphics.c index dbede5a5b..beac9659d 100644 --- a/src/tr1/game/option/option_graphics.c +++ b/src/tr1/game/option/option_graphics.c @@ -50,6 +50,7 @@ typedef enum { OPTION_BRIGHTNESS, OPTION_UI_TEXT_SCALE, OPTION_UI_BAR_SCALE, + OPTION_UI_SCROLL_WRAPAROUND, OPTION_RENDER_MODE, OPTION_RESOLUTION, OPTION_TRAPEZOID_FILTER, @@ -95,6 +96,8 @@ static const GRAPHICS_OPTION_ROW m_GfxOptionRows[] = { GS_ID(DETAIL_FLOAT_FMT) }, { OPTION_UI_BAR_SCALE, GS_ID(DETAIL_UI_BAR_SCALE), GS_ID(DETAIL_FLOAT_FMT) }, + { OPTION_UI_SCROLL_WRAPAROUND, GS_ID(DETAIL_UI_SCROLL_WRAPAROUND), + GS_ID(MISC_ON) }, { OPTION_RENDER_MODE, GS_ID(DETAIL_RENDER_MODE), GS_ID(DETAIL_STRING_FMT) }, { OPTION_RESOLUTION, GS_ID(DETAIL_RESOLUTION), GS_ID(DETAIL_RESOLUTION_FMT) }, @@ -177,6 +180,8 @@ static void M_MenuUp(void) } m_GraphicsMenu.cur_option--; M_UpdateText(); + } else if (g_Config.ui.enable_wraparound) { + M_Reinitialize(m_GfxOptionRows[OPTION_NUMBER_OF - 1].option_name); } } @@ -191,6 +196,8 @@ static void M_MenuDown(void) } m_GraphicsMenu.cur_option++; M_UpdateText(); + } else if (g_Config.ui.enable_wraparound) { + M_Reinitialize(m_GfxOptionRows[0].option_name); } } @@ -323,6 +330,10 @@ static void M_UpdateArrows( m_HideArrowLeft = g_Config.ui.bar_scale <= CONFIG_MIN_BAR_SCALE; m_HideArrowRight = g_Config.ui.bar_scale >= CONFIG_MAX_BAR_SCALE; break; + case OPTION_UI_SCROLL_WRAPAROUND: + m_HideArrowLeft = !g_Config.ui.enable_wraparound; + m_HideArrowRight = g_Config.ui.enable_wraparound; + break; case OPTION_RENDER_MODE: local_right_arrow_offset = RIGHT_ARROW_OFFSET_MAX; m_HideArrowLeft = false; @@ -483,6 +494,11 @@ static void M_ChangeTextOption( Text_ChangeText(value_text, buf); break; + case OPTION_UI_SCROLL_WRAPAROUND: + bool is_enabled = g_Config.ui.enable_wraparound; + Text_ChangeText(value_text, is_enabled ? GS(MISC_ON) : GS(MISC_OFF)); + break; + case OPTION_RENDER_MODE: sprintf( buf, GS(DETAIL_STRING_FMT), @@ -636,6 +652,13 @@ void Option_Graphics_Control(INVENTORY_ITEM *inv_item, const bool is_busy) reset = OPTION_UI_BAR_SCALE; break; + case OPTION_UI_SCROLL_WRAPAROUND: + if (!g_Config.ui.enable_wraparound) { + g_Config.ui.enable_wraparound = true; + reset = OPTION_UI_SCROLL_WRAPAROUND; + } + break; + case OPTION_RENDER_MODE: if (g_Config.rendering.render_mode == GFX_RM_LEGACY) { g_Config.rendering.render_mode = GFX_RM_FRAMEBUFFER; @@ -766,6 +789,13 @@ void Option_Graphics_Control(INVENTORY_ITEM *inv_item, const bool is_busy) reset = OPTION_UI_BAR_SCALE; break; + case OPTION_UI_SCROLL_WRAPAROUND: + if (g_Config.ui.enable_wraparound) { + g_Config.ui.enable_wraparound = false; + reset = OPTION_UI_SCROLL_WRAPAROUND; + } + break; + case OPTION_RENDER_MODE: if (g_Config.rendering.render_mode == GFX_RM_LEGACY) { g_Config.rendering.render_mode = GFX_RM_FRAMEBUFFER; diff --git a/src/tr2/game/ui/dialogs/graphic_settings.c b/src/tr2/game/ui/dialogs/graphic_settings.c index 75ee7a212..7bf02f4f2 100644 --- a/src/tr2/game/ui/dialogs/graphic_settings.c +++ b/src/tr2/game/ui/dialogs/graphic_settings.c @@ -219,6 +219,12 @@ static const M_OPTION m_Options[] = { .delta_fast = 10, }, + { + .option_type = COT_BOOL, + .label_id = GS_ID(DETAIL_UI_SCROLL_WRAPAROUND), + .target = &g_Config.ui.enable_wraparound, + }, + { .option_type = COT_INT32, .label_id = GS_ID(DETAIL_SCALER), From c590824944940aaa02eda8dae06f0c0c1c5be49f Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Wed, 23 Apr 2025 21:07:24 +0100 Subject: [PATCH 06/52] game: prioritize save over load This matches OG behaviour where save is preferred over load when both inputs are detected on the same frame. Resolves #2833. --- docs/tr1/CHANGELOG.md | 1 + docs/tr2/CHANGELOG.md | 1 + src/tr1/game/game/game.c | 6 +++--- src/tr2/game/game.c | 6 +++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index c99cbb79c..818130279 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -10,6 +10,7 @@ - changed the number of static mesh slots from 50 to 256 (#2734) - changed the "enable EIDOS logo" option to disable the Core Design and Bink Video Codec FMVs as well; renamed to "enable legal" (#2741) - changed sprite pickups to respect the water tint if placed underwater (#2673) +- changed save to take priority over load when both inputs are held on the same frame, in line with OG (#2833) - fixed the bilinear filter to not readjust the UVs (#2258) - fixed disabling the cutscenes causing the game to exit (#2743, regression from 4.8) - fixed anisotropy filter causing black lines on certain GPUs (#902) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index bcfd6a666..ed6a4d7cb 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,5 +1,6 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...develop) - ××××-××-×× - added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) +- changed save to take priority over load when both inputs are held on the same frame, in line with OG (#2833) - fixed the selected keyboard/controller layout not being saved (#2830, regression from 1.0) - fixed toggling the PSX FOV option not having an immediate effect (#2831, regression from 1.0) - fixed changing the aspect ratio not updating the current background image (#2832, regression from 1.0) diff --git a/src/tr1/game/game/game.c b/src/tr1/game/game/game.c index 8a9488eb1..85b1e2dd8 100644 --- a/src/tr1/game/game/game.c +++ b/src/tr1/game/game/game.c @@ -142,10 +142,10 @@ GF_COMMAND Game_Control(const bool demo_mode) if (g_Camera.type == CAM_CINEMATIC) { g_OverlayFlag = 0; } else if (g_OverlayFlag > 0) { - if (g_Input.load) { - g_OverlayFlag = -1; - } else if (g_Input.save) { + if (g_Input.save) { g_OverlayFlag = -2; + } else if (g_Input.load) { + g_OverlayFlag = -1; } else { g_OverlayFlag = 0; } diff --git a/src/tr2/game/game.c b/src/tr2/game/game.c index 874a7c157..2269cc5c3 100644 --- a/src/tr2/game/game.c +++ b/src/tr2/game/game.c @@ -114,10 +114,10 @@ GF_COMMAND Game_Control(const bool demo_mode) if (g_OverlayStatus > 0) { if (g_GameFlow.load_save_disabled) { g_OverlayStatus = 0; - } else if (g_Input.load) { - g_OverlayStatus = -1; + } else if (g_Input.save) { + g_OverlayStatus = -2; } else { - g_OverlayStatus = g_Input.save ? -2 : 0; + g_OverlayStatus = g_Input.load ? -1 : 0; } } else { GF_COMMAND gf_cmd; From 3acab0dc34c618a5e50d44afba9d85aeb1a6a634 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Thu, 24 Apr 2025 06:48:48 +0200 Subject: [PATCH 07/52] tr2/ui: improve graphic settings dialog sizing Resolves #2841. --- docs/tr2/CHANGELOG.md | 1 + src/libtrx/game/ui/elements/requester.c | 6 +-- src/tr2/game/ui/dialogs/graphic_settings.c | 63 +++++++++++++++++----- src/tr2/game/ui/dialogs/graphic_settings.h | 2 + 4 files changed, 57 insertions(+), 15 deletions(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index ed6a4d7cb..0273a9c4a 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -4,6 +4,7 @@ - fixed the selected keyboard/controller layout not being saved (#2830, regression from 1.0) - fixed toggling the PSX FOV option not having an immediate effect (#2831, regression from 1.0) - fixed changing the aspect ratio not updating the current background image (#2832, regression from 1.0) +- improved graphic settings dialog sizing (#2841) ## [1.0](https://github.com/LostArtefacts/TRX/compare/tr2-0.10...tr2-1.0) - 2025-04-23 - added support for The Golden Mask (#1621) diff --git a/src/libtrx/game/ui/elements/requester.c b/src/libtrx/game/ui/elements/requester.c index aced27ddc..20103043b 100644 --- a/src/libtrx/game/ui/elements/requester.c +++ b/src/libtrx/game/ui/elements/requester.c @@ -22,9 +22,9 @@ static void M_DownArrow(const UI_REQUESTER_STATE *s); static void M_UpArrow(const UI_REQUESTER_STATE *const s) { UI_BeginHide(s->vis_row == 0); - UI_Spacer(0.0f, 4.0f); + UI_Spacer(0.0f, TR_VERSION == 2 ? 6.0f : 4.0f); UI_BeginAnchor(0.5f, 0.5f); - UI_BeginFixed(0.5f, 1.5f); + UI_BeginFixed(0.5f, TR_VERSION == 2 ? 1.25f : 1.5f); UI_LabelEx("\\{arrow up}", (UI_LABEL_SETTINGS) { .scale = 0.7 }); UI_EndFixed(); UI_EndAnchor(); @@ -40,7 +40,7 @@ static void M_DownArrow(const UI_REQUESTER_STATE *const s) UI_EndFixed(); UI_EndAnchor(); UI_EndHide(); - UI_Spacer(0.0f, 4.0f); + UI_Spacer(0.0f, TR_VERSION == 2 ? 6.0f : 4.0f); } void UI_Requester_Init( diff --git a/src/tr2/game/ui/dialogs/graphic_settings.c b/src/tr2/game/ui/dialogs/graphic_settings.c index 7bf02f4f2..87aa8c043 100644 --- a/src/tr2/game/ui/dialogs/graphic_settings.c +++ b/src/tr2/game/ui/dialogs/graphic_settings.c @@ -13,12 +13,15 @@ #include #include #include +#include #include #include #include #include +#define M_ARROW_SPACING 2.0f + typedef struct { CONFIG_OPTION_TYPE option_type; GAME_STRING_ID label_id; @@ -250,11 +253,28 @@ static const M_OPTION m_Options[] = { }, }; +static int32_t M_GetVisibleRows(void); static uint8_t *M_GetColorComponent(const M_OPTION *option); static M_ENUM_LOOKUP M_GetEnumEntry(const M_OPTION *option); static char *M_FormatRowValue(int32_t row_idx); static bool M_CanChangeValue(int32_t row_idx, int32_t dir); static bool M_RequestChangeValue(int32_t row_idx, int32_t dir); +static float M_GetValueWidth(const UI_GRAPHIC_SETTINGS_STATE *s); + +static int32_t M_GetVisibleRows(void) +{ + const int32_t res_h = + Scaler_CalcInverse(Viewport_GetHeight(), SCALER_TARGET_TEXT); + if (res_h <= 240) { + return 5; + } else if (res_h <= 384) { + return 7; + } else if (res_h < 480) { + return 10; + } else { + return 12; + } +} static uint8_t *M_GetColorComponent(const M_OPTION *const option) { @@ -423,6 +443,26 @@ static bool M_RequestChangeValue(const int32_t row_idx, const int32_t dir) return true; } +static float M_GetValueWidth(const UI_GRAPHIC_SETTINGS_STATE *const s) +{ + // Measure the maximum width of the value label to prevent the entire + // dialog from changing its size as the player changes the sound levels. + float result = -1.0f; + for (int32_t i = 0; i < s->req.max_rows; i++) { + const char *const value = M_FormatRowValue(i); + float value_w; + UI_Label_Measure(value, &value_w, nullptr); + result = MAX(result, value_w); + } + float arrow_w; + UI_Label_Measure("\\{button left}", &arrow_w, nullptr); + result += arrow_w; + UI_Label_Measure("\\{button right}", &arrow_w, nullptr); + result += arrow_w; + result += M_ARROW_SPACING * 2; + return result; +} + void UI_GraphicSettings_Init(UI_GRAPHIC_SETTINGS_STATE *const s) { int32_t row_count = 0; @@ -432,6 +472,7 @@ void UI_GraphicSettings_Init(UI_GRAPHIC_SETTINGS_STATE *const s) UI_Requester_Init(&s->req, row_count, row_count, true); s->req.row_pad = 2.0f; s->req.row_spacing = 0.0f; + s->req.show_arrows = true; } void UI_GraphicSettings_Free(UI_GRAPHIC_SETTINGS_STATE *const s) @@ -441,17 +482,7 @@ void UI_GraphicSettings_Free(UI_GRAPHIC_SETTINGS_STATE *const s) bool UI_GraphicSettings_Control(UI_GRAPHIC_SETTINGS_STATE *const s) { - const int32_t scale = Scaler_GetScale(SCALER_TARGET_TEXT) * 100; - if (scale >= 190) { - UI_Requester_SetVisibleRows(&s->req, 6); - } else if (scale >= 160) { - UI_Requester_SetVisibleRows(&s->req, 8); - } else if (scale >= 120) { - UI_Requester_SetVisibleRows(&s->req, 12); - } else { - UI_Requester_SetVisibleRows(&s->req, 18); - } - + UI_Requester_SetVisibleRows(&s->req, M_GetVisibleRows()); const int32_t choice = UI_Requester_Control(&s->req); if (choice == UI_REQUESTER_CANCEL) { return true; @@ -471,6 +502,8 @@ void UI_GraphicSettings(UI_GRAPHIC_SETTINGS_STATE *const s) UI_BeginModal(0.5f, 0.6f); UI_BeginRequester(&s->req, GS(DETAIL_TITLE)); + const float max_value_w = M_GetValueWidth(s) / g_Config.ui.text_scale; + for (int32_t i = 0; i < s->req.max_rows; i++) { if (!UI_Requester_IsRowVisible(&s->req, i)) { UI_BeginResize(-1.0f, 0.0f); @@ -486,19 +519,25 @@ void UI_GraphicSettings(UI_GRAPHIC_SETTINGS_STATE *const s) UI_Label(GameString_Get(m_Options[i].label_id)); UI_Spacer(20.0f, 0.0f); + UI_BeginResize(max_value_w, -1.0f); + UI_BeginAnchor(1.0f, 0.5f); UI_BeginStackEx((UI_STACK_SETTINGS) { .orientation = UI_STACK_HORIZONTAL, .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, - .spacing = { .h = 5.0f }, + .spacing = { .h = M_ARROW_SPACING }, }); UI_BeginHide(i != sel_row || !M_CanChangeValue(i, -1)); UI_Label("\\{button left}"); UI_EndHide(); + UI_Label(M_FormatRowValue(i)); + UI_BeginHide(i != sel_row || !M_CanChangeValue(i, +1)); UI_Label("\\{button right}"); UI_EndHide(); UI_EndStack(); + UI_EndAnchor(); + UI_EndResize(); UI_EndStack(); UI_EndRequesterRow(&s->req, i); diff --git a/src/tr2/game/ui/dialogs/graphic_settings.h b/src/tr2/game/ui/dialogs/graphic_settings.h index 00d02a526..6f7f053be 100644 --- a/src/tr2/game/ui/dialogs/graphic_settings.h +++ b/src/tr2/game/ui/dialogs/graphic_settings.h @@ -5,6 +5,8 @@ typedef struct { UI_REQUESTER_STATE req; + float arrow_spacing; + float value_w; } UI_GRAPHIC_SETTINGS_STATE; void UI_GraphicSettings_Init(UI_GRAPHIC_SETTINGS_STATE *s); From a6ebaf5c38c2a80b2b481429cf1798b99ce62a3a Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Thu, 24 Apr 2025 23:37:10 +0200 Subject: [PATCH 08/52] docs/tr2: release 1.0.1 --- docs/tr2/CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 0273a9c4a..1577ff805 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,4 +1,6 @@ -## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...develop) - ××××-××-×× +## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...develop) - ××××-××-×× + +## [1.0.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...tr2-1.0.1) - 2025-04-24 - added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) - changed save to take priority over load when both inputs are held on the same frame, in line with OG (#2833) - fixed the selected keyboard/controller layout not being saved (#2830, regression from 1.0) From 10b9bcc7809cb4a64846cbbf21736973caca41cb Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Wed, 23 Apr 2025 23:35:19 +0200 Subject: [PATCH 09/52] option: port sound settings dialog to ui --- data/tr1/ship/cfg/TR1X_strings.json5 | 4 +- data/tr2/ship/cfg/TR2X_strings.json5 | 4 +- docs/tr1/CHANGELOG.md | 1 + docs/tr2/CHANGELOG.md | 1 + src/libtrx/game/music.c | 10 + src/libtrx/game/ui/dialogs/sound_settings.c | 176 ++++++++++++++++++ src/libtrx/game/ui/elements/stack.c | 2 +- .../include/libtrx/game/game_string.def | 4 +- src/libtrx/include/libtrx/game/music/common.h | 12 ++ src/libtrx/include/libtrx/game/sound/common.h | 5 + src/libtrx/include/libtrx/game/ui.h | 1 + .../libtrx/game/ui/dialogs/sound_settings.h | 21 +++ src/libtrx/meson.build | 1 + src/tr1/game/music.c | 14 +- src/tr1/game/music.h | 12 -- src/tr1/game/option/option.c | 3 + src/tr1/game/option/option_sound.c | 172 +++-------------- src/tr1/game/option/option_sound.h | 1 + src/tr1/game/sound.c | 24 +-- src/tr1/game/sound.h | 4 - src/tr2/game/option/option_sound.c | 144 +++----------- src/tr2/game/sound.h | 3 - 22 files changed, 311 insertions(+), 308 deletions(-) create mode 100644 src/libtrx/game/ui/dialogs/sound_settings.c create mode 100644 src/libtrx/include/libtrx/game/ui/dialogs/sound_settings.h diff --git a/data/tr1/ship/cfg/TR1X_strings.json5 b/data/tr1/ship/cfg/TR1X_strings.json5 index b8ea28438..fe26d2eaa 100644 --- a/data/tr1/ship/cfg/TR1X_strings.json5 +++ b/data/tr1/ship/cfg/TR1X_strings.json5 @@ -531,7 +531,9 @@ "PHOTO_MODE_ROTATE_PROMPT": "Rotate camera", "PHOTO_MODE_SNAP_PROMPT": "Take picture", "PHOTO_MODE_TITLE": "Photo Mode", - "SOUND_SET_VOLUMES": "Set Volumes", + "SOUND_DIALOG_MUSIC": "\\{icon sound} Sound", + "SOUND_DIALOG_SOUND": "\\{icon music} Music", + "SOUND_DIALOG_TITLE": "Set Volumes", "STATS_AMMO": "AMMO HITS/USED", "STATS_BASIC_FMT": "%d", "STATS_BONUS_STATISTICS": "Bonus Statistics", diff --git a/data/tr2/ship/cfg/TR2X_strings.json5 b/data/tr2/ship/cfg/TR2X_strings.json5 index abc97764d..e2d2e8c0c 100644 --- a/data/tr2/ship/cfg/TR2X_strings.json5 +++ b/data/tr2/ship/cfg/TR2X_strings.json5 @@ -655,7 +655,9 @@ "PHOTO_MODE_ROTATE_PROMPT": "Rotate camera", "PHOTO_MODE_SNAP_PROMPT": "Take picture", "PHOTO_MODE_TITLE": "Photo Mode", - "SOUND_SET_VOLUMES": "Set Volumes", + "SOUND_DIALOG_MUSIC": "\\{icon sound} Sound", + "SOUND_DIALOG_SOUND": "\\{icon music} Music", + "SOUND_DIALOG_TITLE": "Set Volumes", "STATS_AMMO_HITS": "Hits", "STATS_AMMO_USED": "Ammo Used", "STATS_ASSAULT_FINISH": "Finish", diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index 818130279..c5fde4bd5 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -11,6 +11,7 @@ - changed the "enable EIDOS logo" option to disable the Core Design and Bink Video Codec FMVs as well; renamed to "enable legal" (#2741) - changed sprite pickups to respect the water tint if placed underwater (#2673) - changed save to take priority over load when both inputs are held on the same frame, in line with OG (#2833) +- changed the sound dialog appearance (repositioned and added text labels) - fixed the bilinear filter to not readjust the UVs (#2258) - fixed disabling the cutscenes causing the game to exit (#2743, regression from 4.8) - fixed anisotropy filter causing black lines on certain GPUs (#902) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 1577ff805..06e4e9d5b 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -3,6 +3,7 @@ ## [1.0.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...tr2-1.0.1) - 2025-04-24 - added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) - changed save to take priority over load when both inputs are held on the same frame, in line with OG (#2833) +- changed the sound dialog appearance (repositioned, added text labels and arrows) - fixed the selected keyboard/controller layout not being saved (#2830, regression from 1.0) - fixed toggling the PSX FOV option not having an immediate effect (#2831, regression from 1.0) - fixed changing the aspect ratio not updating the current background image (#2832, regression from 1.0) diff --git a/src/libtrx/game/music.c b/src/libtrx/game/music.c index c3e60a6e6..19d259fdb 100644 --- a/src/libtrx/game/music.c +++ b/src/libtrx/game/music.c @@ -2,6 +2,16 @@ static uint16_t m_MusicTrackFlags[MAX_MUSIC_TRACKS] = {}; +int32_t Music_GetMinVolume(void) +{ + return 0; +} + +int32_t Music_GetMaxVolume(void) +{ + return 10; +} + void Music_ResetTrackFlags(void) { for (int32_t i = 0; i < MAX_MUSIC_TRACKS; i++) { diff --git a/src/libtrx/game/ui/dialogs/sound_settings.c b/src/libtrx/game/ui/dialogs/sound_settings.c new file mode 100644 index 000000000..b5f382308 --- /dev/null +++ b/src/libtrx/game/ui/dialogs/sound_settings.c @@ -0,0 +1,176 @@ +#include "game/ui/dialogs/sound_settings.h" + +#include "config.h" +#include "game/game_string.h" +#include "game/input.h" +#include "game/music.h" +#include "game/sound.h" +#include "game/ui/elements/anchor.h" +#include "game/ui/elements/hide.h" +#include "game/ui/elements/label.h" +#include "game/ui/elements/modal.h" +#include "game/ui/elements/requester.h" +#include "game/ui/elements/resize.h" +#include "game/ui/elements/spacer.h" +#include "game/ui/elements/stack.h" +#include "memory.h" +#include "strings.h" +#include "utils.h" + +typedef struct UI_SOUND_SETTINGS_STATE { + UI_REQUESTER_STATE req; +} UI_SOUND_SETTINGS_STATE; + +typedef enum { + M_ROW_MUSIC = 0, + M_ROW_SOUND = 1, + M_ROW_COUNT = 2, +} M_ROW; + +static const GAME_STRING_ID m_Labels[M_ROW_COUNT] = { + GS_ID(SOUND_DIALOG_SOUND), + GS_ID(SOUND_DIALOG_MUSIC), +}; + +static char *M_FormatRowValue(int32_t row); +static bool M_CanChange(int32_t row, int32_t dir); +static bool M_RequestChange(int32_t row, int32_t dir); + +static char *M_FormatRowValue(const int32_t row) +{ + switch (row) { + case M_ROW_MUSIC: + return String_Format("%2d", g_Config.audio.music_volume); + case M_ROW_SOUND: + return String_Format("%2d", g_Config.audio.sound_volume); + default: + return nullptr; + } +} + +static bool M_CanChange(const int32_t row, const int32_t dir) +{ + switch (row) { + case M_ROW_MUSIC: + if (dir < 0) { + return g_Config.audio.music_volume > Music_GetMinVolume(); + } else if (dir > 0) { + return g_Config.audio.music_volume < Music_GetMaxVolume(); + } + break; + case M_ROW_SOUND: + if (dir < 0) { + return g_Config.audio.sound_volume > Sound_GetMinVolume(); + } else if (dir > 0) { + return g_Config.audio.sound_volume < Sound_GetMaxVolume(); + } + break; + } + return false; +} + +static bool M_RequestChange(const int32_t row, const int32_t dir) +{ + if (!M_CanChange(row, dir)) { + return false; + } + switch (row) { + case M_ROW_MUSIC: + g_Config.audio.music_volume += dir; + Music_SetVolume(g_Config.audio.music_volume); + break; + case M_ROW_SOUND: + g_Config.audio.sound_volume += dir; + Sound_SetMasterVolume(g_Config.audio.sound_volume); + break; + default: + return false; + } + Config_Write(); + Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS); + return true; +} + +UI_SOUND_SETTINGS_STATE *UI_SoundSettings_Init(void) +{ + UI_SOUND_SETTINGS_STATE *s = Memory_Alloc(sizeof(UI_SOUND_SETTINGS_STATE)); + UI_Requester_Init(&s->req, M_ROW_COUNT, M_ROW_COUNT, true); + s->req.row_pad = 2.0f; + return s; +} + +void UI_SoundSettings_Free(UI_SOUND_SETTINGS_STATE *const s) +{ + UI_Requester_Free(&s->req); + Memory_Free(s); +} + +bool UI_SoundSettings_Control(UI_SOUND_SETTINGS_STATE *const s) +{ + const int32_t choice = UI_Requester_Control(&s->req); + if (choice == UI_REQUESTER_CANCEL) { + return true; + } + const int32_t sel = UI_Requester_GetCurrentRow(&s->req); + if (g_InputDB.menu_left && sel >= 0) { + M_RequestChange(sel, -1); + } else if (g_InputDB.menu_right && sel >= 0) { + M_RequestChange(sel, +1); + } + return false; +} + +void UI_SoundSettings(UI_SOUND_SETTINGS_STATE *const s) +{ + const int32_t sel = UI_Requester_GetCurrentRow(&s->req); + UI_BeginModal(0.5f, 0.6f); + UI_BeginRequester(&s->req, GS(SOUND_DIALOG_TITLE)); + + // Measure the maximum width of the value label to prevent the entire + // dialog from changing its size as the player changes the sound levels. + float value_w = -1.0f; + UI_Label_Measure("10", &value_w, nullptr); + + for (int32_t i = 0; i < s->req.max_rows; ++i) { + if (!UI_Requester_IsRowVisible(&s->req, i)) { + UI_BeginResize(-1.0f, 0.0f); + } else { + UI_BeginResize(-1.0f, -1.0f); + } + UI_BeginRequesterRow(&s->req, i); + + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_HORIZONTAL, + .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, + }); + UI_Label(GameString_Get(m_Labels[i])); + UI_Spacer(20.0f, 0.0f); + + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_HORIZONTAL, + .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, + .spacing = { .h = 5.0f }, + }); + UI_BeginHide(i != sel || !M_CanChange(i, -1)); + UI_Label("\\{button left}"); + UI_EndHide(); + + UI_BeginResize(value_w, -1.0f); + UI_BeginAnchor(0.5f, 0.5f); + UI_Label(M_FormatRowValue(i)); + UI_EndAnchor(); + UI_EndResize(); + + UI_BeginHide(i != sel || !M_CanChange(i, +1)); + UI_Label("\\{button right}"); + UI_EndHide(); + UI_EndStack(); + UI_EndStack(); + + UI_EndRequesterRow(&s->req, i); + UI_EndResize(); + } + + UI_EndRequester(&s->req); + UI_EndModal(); +} diff --git a/src/libtrx/game/ui/elements/stack.c b/src/libtrx/game/ui/elements/stack.c index d8a460a33..6638b775b 100644 --- a/src/libtrx/game/ui/elements/stack.c +++ b/src/libtrx/game/ui/elements/stack.c @@ -114,7 +114,7 @@ static void M_Layout( int32_t child_count = 0; float total_child_main_size = 0.0f; UI_NODE *child = node->first_child; - while (child != NULL) { + while (child != nullptr) { switch (data->settings.orientation) { case UI_STACK_HORIZONTAL: total_child_main_size += child->measure_w; diff --git a/src/libtrx/include/libtrx/game/game_string.def b/src/libtrx/include/libtrx/game/game_string.def index 63a70fe0d..b1cadca94 100644 --- a/src/libtrx/include/libtrx/game/game_string.def +++ b/src/libtrx/include/libtrx/game/game_string.def @@ -132,7 +132,9 @@ GS_DEFINE(PASSPORT_MODE_NEW_GAME_JP_PLUS, "Japanese NG+") GS_DEFINE(PASSPORT_STORY_SO_FAR, "Story so far...") GS_DEFINE(PASSPORT_LEGACY_SELECT_LEVEL_1, "Legacy saves do not") GS_DEFINE(PASSPORT_LEGACY_SELECT_LEVEL_2, "support this feature.") -GS_DEFINE(SOUND_SET_VOLUMES, "Set Volumes") +GS_DEFINE(SOUND_DIALOG_TITLE, "Set Volumes") +GS_DEFINE(SOUND_DIALOG_SOUND, "\\{icon music} Music") +GS_DEFINE(SOUND_DIALOG_MUSIC, "\\{icon sound} Sound") GS_DEFINE(OSD_TRAPEZOID_FILTER_ON, "Trapezoid filter enabled") GS_DEFINE(OSD_TRAPEZOID_FILTER_OFF, "Trapezoid filter disabled") GS_DEFINE(DETAIL_INTEGER_FMT, "%d") diff --git a/src/libtrx/include/libtrx/game/music/common.h b/src/libtrx/include/libtrx/game/music/common.h index 68c3c3838..1e6ee1f85 100644 --- a/src/libtrx/include/libtrx/game/music/common.h +++ b/src/libtrx/include/libtrx/game/music/common.h @@ -31,3 +31,15 @@ extern void Music_Unpause(void); void Music_ResetTrackFlags(void); uint16_t Music_GetTrackFlags(int32_t track_idx); void Music_SetTrackFlags(int32_t track, uint16_t flags); + +// Gets the minimum possible game volume. +extern int32_t Music_GetMinVolume(void); + +// Gets the maximum possible game volume. +extern int32_t Music_GetMaxVolume(void); + +// Gets the game volume. +extern int32_t Music_GetVolume(void); + +// Sets the game volume. +extern void Music_SetVolume(int32_t volume); diff --git a/src/libtrx/include/libtrx/game/sound/common.h b/src/libtrx/include/libtrx/game/sound/common.h index 374320fe5..144a322d8 100644 --- a/src/libtrx/include/libtrx/game/sound/common.h +++ b/src/libtrx/include/libtrx/game/sound/common.h @@ -14,6 +14,11 @@ int16_t *Sound_GetSampleLUT(void); SAMPLE_INFO *Sound_GetSampleInfo(SOUND_EFFECT_ID sfx_num); SAMPLE_INFO *Sound_GetSampleInfoByIdx(int32_t info_idx); +extern int32_t Sound_GetMinVolume(void); +extern int32_t Sound_GetMaxVolume(void); +extern int32_t Sound_GetMasterVolume(void); +extern void Sound_SetMasterVolume(int32_t volume); + void Sound_ResetSources(void); void Sound_PauseAll(void); void Sound_UnpauseAll(void); diff --git a/src/libtrx/include/libtrx/game/ui.h b/src/libtrx/include/libtrx/game/ui.h index 811c320db..fd8a7a341 100644 --- a/src/libtrx/include/libtrx/game/ui.h +++ b/src/libtrx/include/libtrx/game/ui.h @@ -10,6 +10,7 @@ #include "./ui/dialogs/play_any_level.h" #include "./ui/dialogs/save_slot.h" #include "./ui/dialogs/select_level.h" +#include "./ui/dialogs/sound_settings.h" #include "./ui/dialogs/stats.h" #include "./ui/elements/anchor.h" #include "./ui/elements/fade.h" diff --git a/src/libtrx/include/libtrx/game/ui/dialogs/sound_settings.h b/src/libtrx/include/libtrx/game/ui/dialogs/sound_settings.h new file mode 100644 index 000000000..d3bbd128d --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/dialogs/sound_settings.h @@ -0,0 +1,21 @@ +// UI dialog for adjusting music and sound volumes +#pragma once + +#include "../common.h" + +typedef struct UI_SOUND_SETTINGS_STATE UI_SOUND_SETTINGS_STATE; + +// state functions +// Initialize the sound settings dialog state +UI_SOUND_SETTINGS_STATE *UI_SoundSettings_Init(void); + +// Free resources used by the sound settings dialog +void UI_SoundSettings_Free(UI_SOUND_SETTINGS_STATE *s); + +// Handle input/control for the sound settings dialog +// Returns true if the dialog should be closed +bool UI_SoundSettings_Control(UI_SOUND_SETTINGS_STATE *s); + +// draw functions +// Render the sound settings dialog +void UI_SoundSettings(UI_SOUND_SETTINGS_STATE *s); diff --git a/src/libtrx/meson.build b/src/libtrx/meson.build index d3866bca2..98396cb73 100644 --- a/src/libtrx/meson.build +++ b/src/libtrx/meson.build @@ -216,6 +216,7 @@ sources = [ 'game/ui/dialogs/play_any_level.c', 'game/ui/dialogs/save_slot.c', 'game/ui/dialogs/select_level.c', + 'game/ui/dialogs/sound_settings.c', 'game/ui/dialogs/stats.c', 'game/ui/elements/anchor.c', 'game/ui/elements/fade.c', diff --git a/src/tr1/game/music.c b/src/tr1/game/music.c index c4700d9f1..6a0467c8a 100644 --- a/src/tr1/game/music.c +++ b/src/tr1/game/music.c @@ -182,12 +182,12 @@ void Music_Unmute(void) M_SyncVolume(m_AudioStreamID); } -int16_t Music_GetVolume(void) +int32_t Music_GetVolume(void) { return m_Volume; } -void Music_SetVolume(int16_t volume) +void Music_SetVolume(int32_t volume) { if (volume != m_Volume) { m_Volume = volume; @@ -195,16 +195,6 @@ void Music_SetVolume(int16_t volume) } } -int16_t Music_GetMinVolume(void) -{ - return 0; -} - -int16_t Music_GetMaxVolume(void) -{ - return 10; -} - void Music_Pause(void) { if (m_AudioStreamID < 0) { diff --git a/src/tr1/game/music.h b/src/tr1/game/music.h index 97de0c9ad..32957af65 100644 --- a/src/tr1/game/music.h +++ b/src/tr1/game/music.h @@ -19,18 +19,6 @@ void Music_Mute(void); // Unmutes the game music. Doesn't change the music volume. void Music_Unmute(void); -// Gets the game volume. -int16_t Music_GetVolume(void); - -// Sets the game volume. Value can be 0-10. -void Music_SetVolume(int16_t volume); - -// Gets the minimum possible game volume. -int16_t Music_GetMinVolume(void); - -// Gets the maximum possible game volume. -int16_t Music_GetMaxVolume(void); - // Returns the currently playing track. Includes looped music. MUSIC_TRACK_ID Music_GetCurrentPlayingTrack(void); diff --git a/src/tr1/game/option/option.c b/src/tr1/game/option/option.c index 957cc80d6..ca778692f 100644 --- a/src/tr1/game/option/option.c +++ b/src/tr1/game/option/option.c @@ -168,6 +168,9 @@ void Option_Draw(INVENTORY_ITEM *const inv_item) case O_COMPASS_OPTION: Option_Compass_Draw(); break; + case O_SOUND_OPTION: + Option_Sound_Draw(inv_item); + break; case O_PICKUP_OPTION_1: case O_PICKUP_OPTION_2: diff --git a/src/tr1/game/option/option_sound.c b/src/tr1/game/option/option_sound.c index 0af6b0a3c..0777a6d6f 100644 --- a/src/tr1/game/option/option_sound.c +++ b/src/tr1/game/option/option_sound.c @@ -1,170 +1,48 @@ #include "game/option/option_sound.h" -#include "game/game_string.h" -#include "game/input.h" -#include "game/music.h" -#include "game/sound.h" -#include "game/text.h" -#include "global/vars.h" - #include +#include -#include +typedef struct { + UI_SOUND_SETTINGS_STATE *ui; +} M_PRIV; -typedef enum { - TEXT_MUSIC_VOLUME = 0, - TEXT_SOUND_VOLUME = 1, - TEXT_TITLE = 2, - TEXT_TITLE_BORDER = 3, - TEXT_LEFT_ARROW = 4, - TEXT_RIGHT_ARROW = 5, - TEXT_NUMBER_OF = 6, - TEXT_OPTION_MIN = TEXT_MUSIC_VOLUME, - TEXT_OPTION_MAX = TEXT_SOUND_VOLUME, -} SOUND_TEXT; +static M_PRIV m_Priv = {}; -static TEXTSTRING *m_Text[TEXT_NUMBER_OF] = {}; - -static void M_InitText(void); - -static void M_InitText(void) +static void M_Init(M_PRIV *const p) { - char buf[20]; + p->ui = UI_SoundSettings_Init(); +} - m_Text[TEXT_LEFT_ARROW] = Text_Create(-45, 0, "\\{button left}"); - m_Text[TEXT_RIGHT_ARROW] = Text_Create(40, 0, "\\{button right}"); - - m_Text[TEXT_TITLE_BORDER] = Text_Create(0, -32, " "); - m_Text[TEXT_TITLE] = Text_Create(0, -30, GS(SOUND_SET_VOLUMES)); - - if (g_Config.audio.music_volume > 10) { - g_Config.audio.music_volume = 10; - } - sprintf(buf, "\\{icon music} %2d", g_Config.audio.music_volume); - m_Text[TEXT_MUSIC_VOLUME] = Text_Create(0, 0, buf); - - if (g_Config.audio.sound_volume > 10) { - g_Config.audio.sound_volume = 10; - } - sprintf(buf, "\\{icon sound} %2d", g_Config.audio.sound_volume); - m_Text[TEXT_SOUND_VOLUME] = Text_Create(0, 25, buf); - - Text_AddBackground(m_Text[g_OptionSelected], 128, 0, 0, 0, TS_REQUESTED); - Text_AddOutline(m_Text[g_OptionSelected], TS_REQUESTED); - Text_AddBackground(m_Text[TEXT_TITLE], 136, 0, 0, 0, TS_HEADING); - Text_AddOutline(m_Text[TEXT_TITLE], TS_HEADING); - Text_AddBackground(m_Text[TEXT_TITLE_BORDER], 140, 85, 0, 0, TS_BACKGROUND); - Text_AddOutline(m_Text[TEXT_TITLE_BORDER], TS_BACKGROUND); - - for (int i = 0; i < TEXT_NUMBER_OF; i++) { - Text_CentreH(m_Text[i], 1); - Text_CentreV(m_Text[i], 1); +static void M_Shutdown(M_PRIV *const p) +{ + if (p->ui != nullptr) { + UI_SoundSettings_Free(p->ui); + p->ui = nullptr; } } -void Option_Sound_Control(INVENTORY_ITEM *inv_item, const bool is_busy) +void Option_Sound_Control(INVENTORY_ITEM *const inv_item, const bool is_busy) { + M_PRIV *const p = &m_Priv; if (is_busy) { return; } - - char buf[20]; - - if (!m_Text[TEXT_MUSIC_VOLUME]) { - M_InitText(); + if (p->ui == nullptr) { + M_Init(p); } + UI_SoundSettings_Control(p->ui); +} - if (g_InputDB.menu_up && g_OptionSelected > TEXT_OPTION_MIN) { - Text_RemoveOutline(m_Text[g_OptionSelected]); - Text_RemoveBackground(m_Text[g_OptionSelected]); - --g_OptionSelected; - Text_AddBackground( - m_Text[g_OptionSelected], 128, 0, 0, 0, TS_REQUESTED); - Text_AddOutline(m_Text[g_OptionSelected], TS_REQUESTED); - Text_SetPos(m_Text[TEXT_LEFT_ARROW], -45, 0); - Text_SetPos(m_Text[TEXT_RIGHT_ARROW], 40, 0); - } - - if (g_InputDB.menu_down && g_OptionSelected < TEXT_OPTION_MAX) { - Text_RemoveOutline(m_Text[g_OptionSelected]); - Text_RemoveBackground(m_Text[g_OptionSelected]); - ++g_OptionSelected; - Text_AddBackground( - m_Text[g_OptionSelected], 128, 0, 0, 0, TS_REQUESTED); - Text_AddOutline(m_Text[g_OptionSelected], TS_REQUESTED); - Text_SetPos(m_Text[TEXT_LEFT_ARROW], -45, 25); - Text_SetPos(m_Text[TEXT_RIGHT_ARROW], 40, 25); - } - - switch (g_OptionSelected) { - case TEXT_MUSIC_VOLUME: - if (g_InputDB.menu_left - && g_Config.audio.music_volume > Music_GetMinVolume()) { - g_Config.audio.music_volume--; - Config_Write(); - Music_SetVolume(g_Config.audio.music_volume); - Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS); - sprintf(buf, "\\{icon music} %2d", g_Config.audio.music_volume); - Text_ChangeText(m_Text[TEXT_MUSIC_VOLUME], buf); - } else if ( - g_InputDB.menu_right - && g_Config.audio.music_volume < Music_GetMaxVolume()) { - g_Config.audio.music_volume++; - Config_Write(); - Music_SetVolume(g_Config.audio.music_volume); - Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS); - sprintf(buf, "\\{icon music} %2d", g_Config.audio.music_volume); - Text_ChangeText(m_Text[TEXT_MUSIC_VOLUME], buf); - } - - Text_Hide( - m_Text[TEXT_LEFT_ARROW], - g_Config.audio.music_volume == Music_GetMinVolume()); - Text_Hide( - m_Text[TEXT_RIGHT_ARROW], - g_Config.audio.music_volume == Music_GetMaxVolume()); - - break; - - case TEXT_SOUND_VOLUME: - if (g_InputDB.menu_left - && g_Config.audio.sound_volume > Sound_GetMinVolume()) { - g_Config.audio.sound_volume--; - Config_Write(); - Sound_SetMasterVolume(g_Config.audio.sound_volume); - Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS); - sprintf(buf, "\\{icon sound} %2d", g_Config.audio.sound_volume); - Text_ChangeText(m_Text[TEXT_SOUND_VOLUME], buf); - } else if ( - g_InputDB.menu_right - && g_Config.audio.sound_volume < Sound_GetMaxVolume()) { - g_Config.audio.sound_volume++; - Config_Write(); - Sound_SetMasterVolume(g_Config.audio.sound_volume); - Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS); - sprintf(buf, "\\{icon sound} %2d", g_Config.audio.sound_volume); - Text_ChangeText(m_Text[TEXT_SOUND_VOLUME], buf); - } - - Text_Hide( - m_Text[TEXT_LEFT_ARROW], - g_Config.audio.sound_volume == Sound_GetMinVolume()); - Text_Hide( - m_Text[TEXT_RIGHT_ARROW], - g_Config.audio.sound_volume == Sound_GetMaxVolume()); - - break; - } - - if (g_InputDB.menu_confirm || g_InputDB.menu_back) { - Option_Sound_Shutdown(); +void Option_Sound_Draw(INVENTORY_ITEM *const inv_item) +{ + M_PRIV *const p = &m_Priv; + if (p->ui != nullptr) { + UI_SoundSettings(p->ui); } } void Option_Sound_Shutdown(void) { - for (int i = 0; i < TEXT_NUMBER_OF; i++) { - Text_Remove(m_Text[i]); - m_Text[i] = nullptr; - } + M_Shutdown(&m_Priv); } diff --git a/src/tr1/game/option/option_sound.h b/src/tr1/game/option/option_sound.h index 9128617d5..ee5f63ba7 100644 --- a/src/tr1/game/option/option_sound.h +++ b/src/tr1/game/option/option_sound.h @@ -3,4 +3,5 @@ #include void Option_Sound_Control(INVENTORY_ITEM *inv_item, bool is_busy); +void Option_Sound_Draw(INVENTORY_ITEM *inv_item); void Option_Sound_Shutdown(void); diff --git a/src/tr1/game/sound.c b/src/tr1/game/sound.c index 3f479d368..7728f2465 100644 --- a/src/tr1/game/sound.c +++ b/src/tr1/game/sound.c @@ -528,18 +528,6 @@ void Sound_StopAll(void) Audio_Sample_CloseAll(); } -void Sound_SetMasterVolume(int8_t volume) -{ - int8_t raw_volume = volume ? 6 * volume + 3 : 0; - m_MasterVolumeDefault = raw_volume & 0x3F; - m_MasterVolume = raw_volume & 0x3F; -} - -int8_t Sound_GetMasterVolume(void) -{ - return (m_MasterVolume - 3) / 6; -} - int32_t Sound_GetMinVolume(void) { return 0; @@ -550,6 +538,18 @@ int32_t Sound_GetMaxVolume(void) return 10; } +void Sound_SetMasterVolume(int32_t volume) +{ + int8_t raw_volume = volume ? 6 * volume + 3 : 0; + m_MasterVolumeDefault = raw_volume & 0x3F; + m_MasterVolume = raw_volume & 0x3F; +} + +int32_t Sound_GetMasterVolume(void) +{ + return (m_MasterVolume - 3) / 6; +} + void Sound_ResetAmbient(void) { M_ResetAmbientLoudness(); diff --git a/src/tr1/game/sound.h b/src/tr1/game/sound.h index 4cd892222..9a403a6c6 100644 --- a/src/tr1/game/sound.h +++ b/src/tr1/game/sound.h @@ -13,10 +13,6 @@ bool Sound_StopEffect(SOUND_EFFECT_ID sfx_num, const XYZ_32 *pos); void Sound_UpdateEffects(void); void Sound_ResetEffects(void); void Sound_StopAmbientSounds(void); -int8_t Sound_GetMasterVolume(void); -void Sound_SetMasterVolume(int8_t volume); -int32_t Sound_GetMinVolume(void); -int32_t Sound_GetMaxVolume(void); void Sound_LoadSamples( size_t num_samples, const char **sample_pointers, size_t *sizes); int32_t Sound_GetMaxSamples(void); diff --git a/src/tr2/game/option/option_sound.c b/src/tr2/game/option/option_sound.c index f6cfab736..f5dec720f 100644 --- a/src/tr2/game/option/option_sound.c +++ b/src/tr2/game/option/option_sound.c @@ -1,132 +1,48 @@ -#include "game/game_string.h" -#include "game/input.h" -#include "game/inventory_ring.h" -#include "game/music.h" #include "game/option/option.h" -#include "game/sound.h" -#include "game/text.h" -#include "global/vars.h" #include -#include +#include -#include +typedef struct { + UI_SOUND_SETTINGS_STATE *ui; +} M_PRIV; -static TEXTSTRING *m_SoundText[4]; +static M_PRIV m_Priv = {}; -static void M_InitText(void); -static void M_ShutdownText(void); - -static void M_InitText(void) +static void M_Init(M_PRIV *const p) { - CLAMPG(g_Config.audio.music_volume, 10); - CLAMPG(g_Config.audio.sound_volume, 10); + p->ui = UI_SoundSettings_Init(); +} - char text[32]; - sprintf(text, "\\{icon music} %2d", g_Config.audio.music_volume); - m_SoundText[0] = Text_Create(0, 0, text); - Text_AddBackground(m_SoundText[0], 128, 0, 0, 0, TS_REQUESTED); - Text_AddOutline(m_SoundText[0], TS_REQUESTED); - - sprintf(text, "\\{icon sound} %2d", g_Config.audio.sound_volume); - m_SoundText[1] = Text_Create(0, 25, text); - - m_SoundText[2] = Text_Create(0, -32, " "); - Text_AddBackground(m_SoundText[2], 140, 85, 0, 0, TS_BACKGROUND); - Text_AddOutline(m_SoundText[2], TS_BACKGROUND); - - m_SoundText[3] = Text_Create(0, -30, GS(SOUND_SET_VOLUMES)); - Text_AddBackground(m_SoundText[3], 136, 0, 0, 0, TS_HEADING); - Text_AddOutline(m_SoundText[3], TS_HEADING); - - for (int32_t i = 0; i < 4; i++) { - Text_CentreH(m_SoundText[i], true); - Text_CentreV(m_SoundText[i], true); +static void M_Shutdown(M_PRIV *const p) +{ + if (p->ui != nullptr) { + UI_SoundSettings_Free(p->ui); + p->ui = nullptr; } } -static void M_ShutdownText(void) +void Option_Sound_Control(INVENTORY_ITEM *const inv_item, const bool is_busy) { - for (int32_t i = 0; i < 4; i++) { - Text_Remove(m_SoundText[i]); - m_SoundText[i] = nullptr; + M_PRIV *const p = &m_Priv; + if (is_busy) { + return; + } + if (p->ui == nullptr) { + M_Init(p); + } + UI_SoundSettings_Control(p->ui); +} + +void Option_Sound_Draw(INVENTORY_ITEM *const inv_item) +{ + M_PRIV *const p = &m_Priv; + if (p->ui != nullptr) { + UI_SoundSettings(p->ui); } } void Option_Sound_Shutdown(void) { - M_ShutdownText(); -} - -void Option_Sound_Control(INVENTORY_ITEM *const item, const bool is_busy) -{ - if (is_busy) { - return; - } - - char text[32]; - - if (m_SoundText[0] == nullptr) { - M_InitText(); - } - - if (g_InputDB.menu_up && g_SoundOptionLine > 0) { - Text_RemoveOutline(m_SoundText[g_SoundOptionLine]); - Text_RemoveBackground(m_SoundText[g_SoundOptionLine]); - g_SoundOptionLine--; - Text_AddBackground( - m_SoundText[g_SoundOptionLine], 128, 0, 0, 0, TS_REQUESTED); - Text_AddOutline(m_SoundText[g_SoundOptionLine], TS_REQUESTED); - } - - if (g_InputDB.menu_down && g_SoundOptionLine < 1) { - Text_RemoveOutline(m_SoundText[g_SoundOptionLine]); - Text_RemoveBackground(m_SoundText[g_SoundOptionLine]); - g_SoundOptionLine++; - Text_AddBackground( - m_SoundText[g_SoundOptionLine], 128, 0, 0, 0, TS_REQUESTED); - Text_AddOutline(m_SoundText[g_SoundOptionLine], TS_REQUESTED); - } - - if (g_SoundOptionLine) { - bool changed = false; - if (g_InputDB.menu_left && g_Config.audio.sound_volume > 0) { - g_Config.audio.sound_volume--; - changed = true; - } else if (g_InputDB.menu_right && g_Config.audio.sound_volume < 10) { - g_Config.audio.sound_volume++; - changed = true; - } - - if (changed) { - sprintf(text, "\\{icon sound} %2d", g_Config.audio.sound_volume); - Text_ChangeText(m_SoundText[1], text); - Sound_SetMasterVolume(g_Config.audio.sound_volume); - Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS); - } - } else { - bool changed = false; - if (g_InputDB.menu_left && g_Config.audio.music_volume > 0) { - g_Config.audio.music_volume--; - changed = true; - } else if (g_InputDB.menu_right && g_Config.audio.music_volume < 10) { - g_Config.audio.music_volume++; - changed = true; - } - - if (changed) { - sprintf(text, "\\{icon music} %2d", g_Config.audio.music_volume); - Text_ChangeText(m_SoundText[0], text); - Music_SetVolume(g_Config.audio.music_volume); - Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS); - } - } - - if (g_InputDB.menu_confirm || g_InputDB.menu_back) { - Option_Sound_Shutdown(); - } -} - -void Option_Sound_Draw(INVENTORY_ITEM *const item) -{ + M_Shutdown(&m_Priv); } diff --git a/src/tr2/game/sound.h b/src/tr2/game/sound.h index 8b0fc32ad..53b5e88ca 100644 --- a/src/tr2/game/sound.h +++ b/src/tr2/game/sound.h @@ -9,9 +9,6 @@ void Sound_Init(void); void Sound_Shutdown(void); -void Sound_SetMasterVolume(int32_t volume); void Sound_UpdateEffects(void); void Sound_StopEffect(SOUND_EFFECT_ID sample_id); void Sound_EndScene(void); -int32_t Sound_GetMinVolume(void); -int32_t Sound_GetMaxVolume(void); From 2536ff55c1172b35d66e02f7bebc50c17e3362db Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 10:05:27 +0200 Subject: [PATCH 10/52] docs: fix formatting and mistyped option name --- docs/MIGRATING.md | 5 +++-- docs/tr1/CHANGELOG.md | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/MIGRATING.md b/docs/MIGRATING.md index aa61e581f..1c9f2deb8 100644 --- a/docs/MIGRATING.md +++ b/docs/MIGRATING.md @@ -6,10 +6,11 @@ 1. **Update fog configuration** If you wish to force your fog settings on player: - - Rename `draw_distance_min` to `fog_start` + - Rename `draw_distance_fade` to `fog_start` - Rename `draw_distance_max` to `fog_end` + If you wish to give the player agency to change the fog: - - Remove `draw_distance_min` and `draw_distance_max` + - Remove `draw_distance_fade` and `draw_distance_max` ### Version 4.7 to 4.8 diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index c5fde4bd5..6ca1234bd 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -5,7 +5,7 @@ - added support for antitriggers, like TR2+ (#2580) - added support for aspect ratio-specific images (#1840) - added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) -- changed the `draw_distance_min` and `draw_distance_max` to `fog_start` and `fog_end` +- changed the `draw_distance_fade` and `draw_distance_max` to `fog_start` and `fog_end` - changed `Select Detail` dialog title to `Graphic Options` - changed the number of static mesh slots from 50 to 256 (#2734) - changed the "enable EIDOS logo" option to disable the Core Design and Bink Video Codec FMVs as well; renamed to "enable legal" (#2741) From eec8f16d5f9f21d47b898bdcd50390187ca28d34 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 11:28:40 +0200 Subject: [PATCH 11/52] tools: download GM assets too --- .gitignore | 7 +++++++ tools/download_assets | 36 ++++++++++++++---------------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 0e5438a15..a7b8c9b27 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,11 @@ src/tr2/subprojects/dwarfstack.wrap data/tr1/ship/data/images/ data/tr2/ship/data/images/ +data/tr2/ship/data/level1.tr2 +data/tr2/ship/data/level2.tr2 +data/tr2/ship/data/level3.tr2 +data/tr2/ship/data/level4.tr2 +data/tr2/ship/data/level5.tr2 +data/tr2/ship/data/main_gm.sfx +data/tr2/ship/data/title_gm.tr2 data/tr2/ship/music/ diff --git a/tools/download_assets b/tools/download_assets index ecc2292c1..6b20c3b9f 100755 --- a/tools/download_assets +++ b/tools/download_assets @@ -42,41 +42,33 @@ def extract_zip(zip_path: Path, dest_dir: Path) -> None: z.extractall(dest_dir) -def download_assets(assets: list[tuple[str, Path]]) -> None: +def download_assets(asset_urls: list[str], target_dir: Path) -> None: with tempfile.TemporaryDirectory() as tmpdir_str: tmpdir = Path(tmpdir_str) - for url, dest in assets: + for url in asset_urls: filename = Path(url).name local_zip = tmpdir / filename download_to_file(url, local_zip) - extract_zip(local_zip, dest) + extract_zip(local_zip, target_dir) print("Asset download and extraction complete.") def main() -> None: args = parse_args() - assets: dict[int, list[tuple[str, Path]]] = { - 1: [ - ( - "https://lostartefacts.dev/aux/tr1x/main.zip", - Path("data/tr1/ship"), - ) - ], + asset_urls_map: dict[int, list[str]] = { + 1: ["https://lostartefacts.dev/aux/tr1x/main.zip"], 2: [ - ( - "https://lostartefacts.dev/aux/tr2x/main.zip", - Path("data/tr2/ship"), - ) + "https://lostartefacts.dev/aux/tr2x/main.zip", + "https://lostartefacts.dev/aux/tr2x/trgm.zip", ], } - match str(args.game_version): - case "1": - download_assets(assets[1]) - case "2": - download_assets(assets[2]) - case "all": - download_assets(assets[1]) - download_assets(assets[2]) + + versions = {"1": [1], "2": [2], "all": [1, 2]}[args.game_version] + for version in versions: + download_assets( + asset_urls_map[version], + target_dir=PROJECT_PATHS[version].shipped_data_dir, + ) if __name__ == "__main__": From 15b758c57d8412509861e5ff74539454a5d9cc18 Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Sat, 26 Apr 2025 09:57:06 +0100 Subject: [PATCH 12/52] items: replace items by index rather than room Carried items use NO_ROOM so were not included when replacing guns with ammo. This ensures everything is checked when replacing IDs. Resolves #2850. Resolves #2856. --- docs/tr2/CHANGELOG.md | 2 ++ docs/tr2/README.md | 1 + src/libtrx/game/items.c | 14 +++++--------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 06e4e9d5b..fbea5db6c 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,4 +1,6 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...develop) - ××××-××-×× +- fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) +- fixed guns carried by enemies not being converted to ammo if Lara starts the level with the gun and the game has later been reloaded (#2850, regression from 1.0) ## [1.0.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...tr2-1.0.1) - 2025-04-24 - added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) diff --git a/docs/tr2/README.md b/docs/tr2/README.md index 9faec4fd4..d0fe8a2f2 100644 --- a/docs/tr2/README.md +++ b/docs/tr2/README.md @@ -254,6 +254,7 @@ However, you can easily download them manually from these urls: - fixed Floating Islands mystic plaque inventory rotation - fixed pushblocks being rotated when Lara grabs them, most noticeable if asymmetric textures have been used - fixed being able to use hotkeys in the end-level statistics screen +- fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level - fixed destroyed gondolas appearing embedded in the ground after loading a save - improved the animation of Lara's braid diff --git a/src/libtrx/game/items.c b/src/libtrx/game/items.c index a2698b37b..f92b67299 100644 --- a/src/libtrx/game/items.c +++ b/src/libtrx/game/items.c @@ -219,15 +219,11 @@ int32_t Item_GlobalReplace( { int32_t changed = 0; - for (int32_t i = 0; i < Room_GetCount(); i++) { - int16_t item_num = Room_Get(i)->item_num; - while (item_num != NO_ITEM) { - ITEM *const item = &m_Items[item_num]; - if (item->object_id == src_obj_id) { - item->object_id = dst_obj_id; - changed++; - } - item_num = item->next_item; + for (int32_t item_num = 0; item_num < m_MaxUsedItemCount; item_num++) { + ITEM *const item = &m_Items[item_num]; + if (item->object_id == src_obj_id) { + item->object_id = dst_obj_id; + changed++; } } From 607ac811f0b091571dbabdf20c0311cec77cf8ae Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 11:55:23 +0200 Subject: [PATCH 13/52] tr2/viewport: fix screenshots at wrong resolution Resolves #2845. --- docs/tr2/CHANGELOG.md | 1 + src/tr2/game/viewport.c | 11 +++++++---- src/tr2/game/viewport.h | 5 ++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index fbea5db6c..7df45025d 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...develop) - ××××-××-×× - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) - fixed guns carried by enemies not being converted to ammo if Lara starts the level with the gun and the game has later been reloaded (#2850, regression from 1.0) +- fixed 1920x1080 screenshots in 16:9 aspect mode being saved as 1919x1080 (#2845, regression from 0.8) ## [1.0.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...tr2-1.0.1) - 2025-04-24 - added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) diff --git a/src/tr2/game/viewport.c b/src/tr2/game/viewport.c index 245e1c11c..424039f73 100644 --- a/src/tr2/game/viewport.c +++ b/src/tr2/game/viewport.c @@ -125,20 +125,23 @@ void Viewport_Reset(void) VIEWPORT *const vp = &m_Viewport; switch (g_Config.rendering.aspect_mode) { case AM_4_3: - vp->render_ar = 4.0 / 3.0; + vp->render_ar.w = 4; + vp->render_ar.h = 3; break; case AM_16_9: - vp->render_ar = 16.0 / 9.0; + vp->render_ar.w = 16; + vp->render_ar.h = 9; break; case AM_ANY: - vp->render_ar = size.w / (double)size.h; + vp->render_ar.w = size.w; + vp->render_ar.h = size.h; break; } vp->width = size.w / g_Config.rendering.scaler; vp->height = size.h / g_Config.rendering.scaler; if (g_Config.rendering.aspect_mode != AM_ANY) { - vp->width = vp->height * vp->render_ar; + vp->width = vp->height * vp->render_ar.w / vp->render_ar.h; } vp->near_z = Output_GetNearZ() >> W2V_SHIFT; diff --git a/src/tr2/game/viewport.h b/src/tr2/game/viewport.h index 9af7d0105..8ec3d7485 100644 --- a/src/tr2/game/viewport.h +++ b/src/tr2/game/viewport.h @@ -8,7 +8,10 @@ typedef struct { int32_t near_z; int32_t far_z; int16_t view_angle; - double render_ar; + struct { + int32_t w; + int32_t h; + } render_ar; // TODO: remove most of these variables if possible struct { From 96b86b1605f6d99e2cd740b501b17f8ad0773529 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 13:18:45 +0200 Subject: [PATCH 14/52] docs/tr2: fix changelog merge mistake --- docs/tr2/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 7df45025d..b3b0c1eb2 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,4 +1,5 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...develop) - ××××-××-×× +- changed the sound dialog appearance (repositioned, added text labels and arrows) - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) - fixed guns carried by enemies not being converted to ammo if Lara starts the level with the gun and the game has later been reloaded (#2850, regression from 1.0) - fixed 1920x1080 screenshots in 16:9 aspect mode being saved as 1919x1080 (#2845, regression from 0.8) @@ -6,7 +7,6 @@ ## [1.0.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...tr2-1.0.1) - 2025-04-24 - added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) - changed save to take priority over load when both inputs are held on the same frame, in line with OG (#2833) -- changed the sound dialog appearance (repositioned, added text labels and arrows) - fixed the selected keyboard/controller layout not being saved (#2830, regression from 1.0) - fixed toggling the PSX FOV option not having an immediate effect (#2831, regression from 1.0) - fixed changing the aspect ratio not updating the current background image (#2832, regression from 1.0) From 83ac9514cbc8b5f866d2dd02a4ffc05e2525fbcb Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Sat, 26 Apr 2025 10:20:06 +0100 Subject: [PATCH 15/52] tr2/objects/door: prevent Lara voiding in closed doors This uses the same approach as TR1 to avoid Lara voiding in closing/ closed doors that are not placed on portals. Resovles #2848. --- docs/tr2/CHANGELOG.md | 1 + docs/tr2/README.md | 1 + src/tr2/game/objects/general/door.c | 59 ++++++++++++++++++++++------- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index b3b0c1eb2..fa31d551c 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,5 +1,6 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...develop) - ××××-××-×× - changed the sound dialog appearance (repositioned, added text labels and arrows) +- fixed Lara voiding if she stops on a tile with a closing door, and the door isn't on a portal (#2848) - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) - fixed guns carried by enemies not being converted to ammo if Lara starts the level with the gun and the game has later been reloaded (#2850, regression from 1.0) - fixed 1920x1080 screenshots in 16:9 aspect mode being saved as 1919x1080 (#2845, regression from 0.8) diff --git a/docs/tr2/README.md b/docs/tr2/README.md index d0fe8a2f2..54d53df6a 100644 --- a/docs/tr2/README.md +++ b/docs/tr2/README.md @@ -256,6 +256,7 @@ However, you can easily download them manually from these urls: - fixed being able to use hotkeys in the end-level statistics screen - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level - fixed destroyed gondolas appearing embedded in the ground after loading a save +- fixed Lara voiding if she stops on a tile with a closing door, and the door isn't on a portal - improved the animation of Lara's braid #### Cheats diff --git a/src/tr2/game/objects/general/door.c b/src/tr2/game/objects/general/door.c index 819c8fb5f..e97ec0cd5 100644 --- a/src/tr2/game/objects/general/door.c +++ b/src/tr2/game/objects/general/door.c @@ -21,6 +21,8 @@ static SECTOR *M_GetRoomRelSector( static void M_InitialisePortal( const ROOM *room, const ITEM *item, int32_t sector_dx, int32_t sector_dz, DOORPOS_DATA *door_pos); +static bool M_LaraDoorCollision(const SECTOR *sector); +static void M_Check(DOORPOS_DATA *d); static void M_Shut(DOORPOS_DATA *d); static void M_Open(DOORPOS_DATA *d); static void M_Setup(OBJECT *obj); @@ -38,7 +40,32 @@ static SECTOR *M_GetRoomRelSector( return Room_GetUnitSector(room, sector.x, sector.z); } -static void Door_Shut(DOORPOS_DATA *const d) +static bool M_LaraDoorCollision(const SECTOR *const sector) +{ + // Check if Lara is on the same tile as the invisible block. + const ITEM *const lara = Lara_GetItem(); + if (lara == nullptr) { + return false; + } + + int16_t room_num = lara->room_num; + const SECTOR *const lara_sector = + Room_GetSector(lara->pos.x, lara->pos.y, lara->pos.z, &room_num); + return lara_sector == sector; +} + +static void M_Check(DOORPOS_DATA *const d) +{ + // Forcefully remove the invisible block if Lara happens to occupy the same + // tile. This ensures that Lara doesn't void if a timed door happens to + // close right on her, or the player loads the game while standing on a + // closed door's block tile. + if (M_LaraDoorCollision(d->sector)) { + M_Open(d); + } +} + +static void M_Shut(DOORPOS_DATA *const d) { SECTOR *const sector = d->sector; if (d->sector == nullptr) { @@ -61,7 +88,7 @@ static void Door_Shut(DOORPOS_DATA *const d) } } -static void Door_Open(DOORPOS_DATA *const d) +static void M_Open(DOORPOS_DATA *const d) { if (d->sector == nullptr) { return; @@ -137,8 +164,8 @@ static void M_Initialise(const int16_t item_num) } room_num = door->d1.sector->portal_room.wall; - Door_Shut(&door->d1); - Door_Shut(&door->d1flip); + M_Shut(&door->d1); + M_Shut(&door->d1flip); if (room_num == NO_ROOM) { door->d2.sector = nullptr; @@ -153,8 +180,8 @@ static void M_Initialise(const int16_t item_num) M_InitialisePortal(room, item, 0, 0, &door->d2flip); } - Door_Shut(&door->d2); - Door_Shut(&door->d2flip); + M_Shut(&door->d2); + M_Shut(&door->d2flip); const int16_t prev_room = item->room_num; Item_NewRoom(item_num, room_num); @@ -171,22 +198,26 @@ static void M_Control(const int16_t item_num) if (item->current_anim_state == DOOR_STATE_CLOSED) { item->goal_anim_state = DOOR_STATE_OPEN; } else { - Door_Open(&data->d1); - Door_Open(&data->d2); - Door_Open(&data->d1flip); - Door_Open(&data->d2flip); + M_Open(&data->d1); + M_Open(&data->d2); + M_Open(&data->d1flip); + M_Open(&data->d2flip); } } else { if (item->current_anim_state == DOOR_STATE_OPEN) { item->goal_anim_state = DOOR_STATE_CLOSED; } else { - Door_Shut(&data->d1); - Door_Shut(&data->d2); - Door_Shut(&data->d1flip); - Door_Shut(&data->d2flip); + M_Shut(&data->d1); + M_Shut(&data->d2); + M_Shut(&data->d1flip); + M_Shut(&data->d2flip); } } + M_Check(&data->d1); + M_Check(&data->d2); + M_Check(&data->d1flip); + M_Check(&data->d2flip); Item_Animate(item); } From 890c7f76bb2964df83969313d3d4109bccb1e71a Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Sat, 26 Apr 2025 10:21:03 +0100 Subject: [PATCH 16/52] door: move door module to TRX This moves the door module fully to TRX as the logic is identical in both games. --- .../game/objects/general/door.c | 60 +++-- .../include/libtrx/game/objects/common.h | 1 + src/libtrx/meson.build | 1 + src/tr1/game/objects/common.h | 1 - src/tr1/game/objects/general/door.c | 253 ------------------ src/tr1/global/types.h | 6 - src/tr1/meson.build | 1 - src/tr2/game/objects/common.h | 1 - src/tr2/global/types_decomp.h | 6 - src/tr2/meson.build | 1 - 10 files changed, 34 insertions(+), 297 deletions(-) rename src/{tr2 => libtrx}/game/objects/general/door.c (86%) delete mode 100644 src/tr1/game/objects/general/door.c diff --git a/src/tr2/game/objects/general/door.c b/src/libtrx/game/objects/general/door.c similarity index 86% rename from src/tr2/game/objects/general/door.c rename to src/libtrx/game/objects/general/door.c index e97ec0cd5..e81c4ca39 100644 --- a/src/tr2/game/objects/general/door.c +++ b/src/libtrx/game/objects/general/door.c @@ -1,13 +1,16 @@ -#include "game/box.h" -#include "game/items.h" -#include "game/objects/common.h" -#include "game/room.h" -#include "global/vars.h" +#include "game/objects/general/door.h" -#include -#include -#include -#include +#include "game/game_buf.h" +#include "game/lara/common.h" +#include "game/objects/common.h" +#include "game/pathing.h" +#include "game/rooms.h" + +typedef struct { + SECTOR *sector; + SECTOR old_sector; + int16_t box_num; +} DOORPOS_DATA; typedef struct { DOORPOS_DATA d1; @@ -82,7 +85,7 @@ static void M_Shut(DOORPOS_DATA *const d) sector->portal_room.pit = NO_ROOM_NEG; sector->portal_room.wall = NO_ROOM; - const int16_t box_num = d->block; + const int16_t box_num = d->box_num; if (box_num != NO_BOX) { Box_GetBox(box_num)->overlap_index |= BOX_BLOCKED; } @@ -96,7 +99,7 @@ static void M_Open(DOORPOS_DATA *const d) *d->sector = d->old_sector; - const int16_t box_num = d->block; + const int16_t box_num = d->box_num; if (box_num != NO_BOX) { Box_GetBox(box_num)->overlap_index &= ~BOX_BLOCKED; } @@ -117,10 +120,11 @@ static void M_InitialisePortal( } int16_t box_num = sector->box; - if (!(Box_GetBox(box_num)->overlap_index & BOX_BLOCKABLE)) { + const BOX_INFO *const box = Box_GetBox(box_num); + if ((box->overlap_index & BOX_BLOCKABLE) == 0) { box_num = NO_BOX; } - door_pos->block = box_num; + door_pos->box_num = box_num; door_pos->old_sector = *door_pos->sector; } @@ -192,32 +196,32 @@ static void M_Initialise(const int16_t item_num) static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); - DOOR_DATA *const data = item->data; + DOOR_DATA *const door = item->data; if (Item_IsTriggerActive(item)) { if (item->current_anim_state == DOOR_STATE_CLOSED) { item->goal_anim_state = DOOR_STATE_OPEN; } else { - M_Open(&data->d1); - M_Open(&data->d2); - M_Open(&data->d1flip); - M_Open(&data->d2flip); + M_Open(&door->d1); + M_Open(&door->d2); + M_Open(&door->d1flip); + M_Open(&door->d2flip); } } else { if (item->current_anim_state == DOOR_STATE_OPEN) { item->goal_anim_state = DOOR_STATE_CLOSED; } else { - M_Shut(&data->d1); - M_Shut(&data->d2); - M_Shut(&data->d1flip); - M_Shut(&data->d2flip); + M_Shut(&door->d1); + M_Shut(&door->d2); + M_Shut(&door->d1flip); + M_Shut(&door->d2flip); } } - M_Check(&data->d1); - M_Check(&data->d2); - M_Check(&data->d1flip); - M_Check(&data->d2flip); + M_Check(&door->d1); + M_Check(&door->d2); + M_Check(&door->d1flip); + M_Check(&door->d2flip); Item_Animate(item); } @@ -237,8 +241,8 @@ void Door_Collision( if (coll->enable_baddie_push) { Lara_Push( item, coll, - item->current_anim_state != item->goal_anim_state ? coll->enable_hit - : false, + coll->enable_hit + && item->current_anim_state != item->goal_anim_state, true); } } diff --git a/src/libtrx/include/libtrx/game/objects/common.h b/src/libtrx/include/libtrx/game/objects/common.h index f65578f14..18de887cd 100644 --- a/src/libtrx/include/libtrx/game/objects/common.h +++ b/src/libtrx/include/libtrx/game/objects/common.h @@ -36,6 +36,7 @@ void Object_SwapMesh( ANIM *Object_GetAnim(const OBJECT *obj, int32_t anim_idx); ANIM_BONE *Object_GetBone(const OBJECT *obj, int32_t bone_idx); +extern void Object_DrawUnclippedItem(const ITEM *item); extern void Object_DrawMesh(int32_t mesh_idx, int32_t clip, bool interpolated); void Object_DrawInterpolatedObject( diff --git a/src/libtrx/meson.build b/src/libtrx/meson.build index 98396cb73..d93000c31 100644 --- a/src/libtrx/meson.build +++ b/src/libtrx/meson.build @@ -176,6 +176,7 @@ sources = [ 'game/objects/general/bridge_flat.c', 'game/objects/general/bridge_tilt1.c', 'game/objects/general/bridge_tilt2.c', + 'game/objects/general/door.c', 'game/objects/general/drawbridge.c', 'game/objects/general/trapdoor.c', 'game/objects/names.c', diff --git a/src/tr1/game/objects/common.h b/src/tr1/game/objects/common.h index dd39f97e8..5b9da9fdc 100644 --- a/src/tr1/game/objects/common.h +++ b/src/tr1/game/objects/common.h @@ -12,7 +12,6 @@ void Object_DrawDummyItem(const ITEM *item); void Object_DrawSpriteItem(const ITEM *item); void Object_DrawPickupItem(const ITEM *item); void Object_DrawAnimatingItem(const ITEM *item); -void Object_DrawUnclippedItem(const ITEM *item); void Object_SetMeshReflective( GAME_OBJECT_ID obj_id, int32_t mesh_idx, bool enabled); void Object_SetReflective(GAME_OBJECT_ID obj_id, bool enabled); diff --git a/src/tr1/game/objects/general/door.c b/src/tr1/game/objects/general/door.c deleted file mode 100644 index 5cbe0bfcc..000000000 --- a/src/tr1/game/objects/general/door.c +++ /dev/null @@ -1,253 +0,0 @@ -#include "game/box.h" -#include "game/items.h" -#include "game/lara/common.h" -#include "game/objects/common.h" -#include "game/room.h" -#include "global/vars.h" - -#include -#include -#include -#include - -typedef struct { - DOORPOS_DATA d1; - DOORPOS_DATA d1flip; - DOORPOS_DATA d2; - DOORPOS_DATA d2flip; -} DOOR_DATA; - -static SECTOR *M_GetRoomRelSector( - const ROOM *room, const ITEM *item, int32_t sector_dx, int32_t sector_dz); -static void M_InitialisePortal( - const ROOM *room, const ITEM *item, int32_t sector_dx, int32_t sector_dz, - DOORPOS_DATA *door_pos); - -static bool M_LaraDoorCollision(const SECTOR *sector); -static void M_Check(DOORPOS_DATA *d); -static void M_Shut(DOORPOS_DATA *d); -static void M_Open(DOORPOS_DATA *d); -static void M_Setup(OBJECT *obj); -static void M_Initialise(int16_t item_num); -static void M_Control(int16_t item_num); - -static SECTOR *M_GetRoomRelSector( - const ROOM *const room, const ITEM *item, const int32_t sector_dx, - const int32_t sector_dz) -{ - const XZ_32 sector = { - .x = ((item->pos.x - room->pos.x) >> WALL_SHIFT) + sector_dx, - .z = ((item->pos.z - room->pos.z) >> WALL_SHIFT) + sector_dz, - }; - return Room_GetUnitSector(room, sector.x, sector.z); -} - -static void M_InitialisePortal( - const ROOM *const room, const ITEM *const item, const int32_t sector_dx, - const int32_t sector_dz, DOORPOS_DATA *const door_pos) -{ - door_pos->sector = M_GetRoomRelSector(room, item, sector_dx, sector_dz); - - const SECTOR *sector = door_pos->sector; - - const int16_t room_num = sector->portal_room.wall; - if (room_num != NO_ROOM) { - sector = - M_GetRoomRelSector(Room_Get(room_num), item, sector_dx, sector_dz); - } - - int16_t box_num = sector->box; - if (!(Box_GetBox(box_num)->overlap_index & BOX_BLOCKABLE)) { - box_num = NO_BOX; - } - door_pos->block = box_num; - door_pos->old_sector = *door_pos->sector; -} - -static bool M_LaraDoorCollision(const SECTOR *const sector) -{ - // Check if Lara is on the same tile as the invisible block. - if (g_LaraItem == nullptr) { - return false; - } - - int16_t room_num = g_LaraItem->room_num; - const SECTOR *const lara_sector = Room_GetSector( - g_LaraItem->pos.x, g_LaraItem->pos.y, g_LaraItem->pos.z, &room_num); - return lara_sector == sector; -} - -static void M_Check(DOORPOS_DATA *const d) -{ - // Forcefully remove the invisible block if Lara happens to occupy the same - // tile. This ensures that Lara doesn't void if a timed door happens to - // close right on her, or the player loads the game while standing on a - // closed door's block tile. - if (M_LaraDoorCollision(d->sector)) { - M_Open(d); - } -} - -static void M_Shut(DOORPOS_DATA *const d) -{ - // Change the level geometry so that the door tile is impassable. - SECTOR *const sector = d->sector; - if (sector == nullptr) { - return; - } - - sector->box = NO_BOX; - sector->floor.height = NO_HEIGHT; - sector->ceiling.height = NO_HEIGHT; - sector->floor.tilt = 0; - sector->ceiling.tilt = 0; - sector->portal_room.sky = NO_ROOM; - sector->portal_room.pit = NO_ROOM; - sector->portal_room.wall = NO_ROOM; - - const int16_t box_num = d->block; - if (box_num != NO_BOX) { - Box_GetBox(box_num)->overlap_index |= BOX_BLOCKED; - } -} - -static void M_Open(DOORPOS_DATA *const d) -{ - // Restore the level geometry so that the door tile is passable. - SECTOR *const sector = d->sector; - if (!sector) { - return; - } - - *sector = d->old_sector; - - const int16_t box_num = d->block; - if (box_num != NO_BOX) { - Box_GetBox(box_num)->overlap_index &= ~BOX_BLOCKED; - } -} - -static void M_Setup(OBJECT *const obj) -{ - obj->initialise_func = M_Initialise; - obj->control_func = M_Control; - obj->draw_func = Object_DrawUnclippedItem; - obj->collision_func = Door_Collision; - obj->save_anim = 1; - obj->save_flags = 1; -} - -static void M_Initialise(const int16_t item_num) -{ - ITEM *const item = Item_Get(item_num); - DOOR_DATA *const door = GameBuf_Alloc(sizeof(DOOR_DATA), GBUF_ITEM_DATA); - item->data = door; - - int32_t dx = 0; - int32_t dz = 0; - if (item->rot.y == 0) { - dz = -1; - } else if (item->rot.y == -DEG_180) { - dz = 1; - } else if (item->rot.y == DEG_90) { - dx = -1; - } else { - dx = 1; - } - - int16_t room_num = item->room_num; - const ROOM *room = Room_Get(room_num); - M_InitialisePortal(room, item, dx, dz, &door->d1); - - if (room->flipped_room == -1) { - door->d1flip.sector = nullptr; - } else { - room = Room_Get(room->flipped_room); - M_InitialisePortal(room, item, dx, dz, &door->d1flip); - } - - room_num = door->d1.sector->portal_room.wall; - M_Shut(&door->d1); - M_Shut(&door->d1flip); - - if (room_num == NO_ROOM) { - door->d2.sector = nullptr; - door->d2flip.sector = nullptr; - return; - } - - room = Room_Get(room_num); - M_InitialisePortal(room, item, 0, 0, &door->d2); - if (room->flipped_room == -1) { - door->d2flip.sector = nullptr; - } else { - room = Room_Get(room->flipped_room); - M_InitialisePortal(room, item, 0, 0, &door->d2flip); - } - - M_Shut(&door->d2); - M_Shut(&door->d2flip); - - const int16_t prev_room = item->room_num; - Item_NewRoom(item_num, room_num); - item->room_num = prev_room; -} - -static void M_Control(const int16_t item_num) -{ - ITEM *const item = Item_Get(item_num); - DOOR_DATA *door = item->data; - - if (Item_IsTriggerActive(item)) { - if (item->current_anim_state == DOOR_STATE_CLOSED) { - item->goal_anim_state = DOOR_STATE_OPEN; - } else { - M_Open(&door->d1); - M_Open(&door->d2); - M_Open(&door->d1flip); - M_Open(&door->d2flip); - } - } else { - if (item->current_anim_state == DOOR_STATE_OPEN) { - item->goal_anim_state = DOOR_STATE_CLOSED; - } else { - M_Shut(&door->d1); - M_Shut(&door->d2); - M_Shut(&door->d1flip); - M_Shut(&door->d2flip); - } - } - - M_Check(&door->d1); - M_Check(&door->d2); - M_Check(&door->d1flip); - M_Check(&door->d2flip); - Item_Animate(item); -} - -void Door_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll) -{ - ITEM *const item = Item_Get(item_num); - if (!Lara_TestBoundsCollide(item, coll->radius)) { - return; - } - if (!Collide_TestCollision(item, lara_item)) { - return; - } - if (coll->enable_baddie_push) { - if (item->current_anim_state != item->goal_anim_state) { - Lara_Push(item, coll, coll->enable_hit, true); - } else { - Lara_Push(item, coll, false, true); - } - } -} - -REGISTER_OBJECT(O_DOOR_TYPE_1, M_Setup) -REGISTER_OBJECT(O_DOOR_TYPE_2, M_Setup) -REGISTER_OBJECT(O_DOOR_TYPE_3, M_Setup) -REGISTER_OBJECT(O_DOOR_TYPE_4, M_Setup) -REGISTER_OBJECT(O_DOOR_TYPE_5, M_Setup) -REGISTER_OBJECT(O_DOOR_TYPE_6, M_Setup) -REGISTER_OBJECT(O_DOOR_TYPE_7, M_Setup) -REGISTER_OBJECT(O_DOOR_TYPE_8, M_Setup) diff --git a/src/tr1/global/types.h b/src/tr1/global/types.h index 54dd99625..6b7384d57 100644 --- a/src/tr1/global/types.h +++ b/src/tr1/global/types.h @@ -123,12 +123,6 @@ typedef struct { }; } PHD_VBUF; -typedef struct { - SECTOR *sector; - SECTOR old_sector; - int16_t block; -} DOORPOS_DATA; - typedef struct { PASSPORT_MODE passport_selection; int32_t select_save_slot; diff --git a/src/tr1/meson.build b/src/tr1/meson.build index 277258aec..71f6c3254 100644 --- a/src/tr1/meson.build +++ b/src/tr1/meson.build @@ -197,7 +197,6 @@ sources = [ 'game/objects/general/cabin.c', 'game/objects/general/camera_target.c', 'game/objects/general/cog.c', - 'game/objects/general/door.c', 'game/objects/general/earthquake.c', 'game/objects/general/keyhole.c', 'game/objects/general/moving_bar.c', diff --git a/src/tr2/game/objects/common.h b/src/tr2/game/objects/common.h index 15a9e8cc0..65911e138 100644 --- a/src/tr2/game/objects/common.h +++ b/src/tr2/game/objects/common.h @@ -6,7 +6,6 @@ void Object_DrawDummyItem(const ITEM *item); void Object_DrawAnimatingItem(const ITEM *item); -void Object_DrawUnclippedItem(const ITEM *item); void Object_DrawSpriteItem(const ITEM *item); void Object_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); diff --git a/src/tr2/global/types_decomp.h b/src/tr2/global/types_decomp.h index 8427402b1..3d1825df8 100644 --- a/src/tr2/global/types_decomp.h +++ b/src/tr2/global/types_decomp.h @@ -120,12 +120,6 @@ typedef enum { GFE_REMOVE_AMMO = 22, } GF_EVENTS; -typedef struct { - SECTOR *sector; - SECTOR old_sector; - int16_t block; -} DOORPOS_DATA; - typedef enum { TRAP_SET = 0, TRAP_ACTIVATE = 1, diff --git a/src/tr2/meson.build b/src/tr2/meson.build index ea203d6bf..80c5e9c77 100644 --- a/src/tr2/meson.build +++ b/src/tr2/meson.build @@ -195,7 +195,6 @@ sources = [ 'game/objects/general/cutscene_player.c', 'game/objects/general/detonator.c', 'game/objects/general/ding_dong.c', - 'game/objects/general/door.c', 'game/objects/general/earthquake.c', 'game/objects/general/final_cutscene.c', 'game/objects/general/final_level_counter.c', From dc41197cd6d83c9efe7b68628467c54aa4b763ff Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 09:47:36 +0200 Subject: [PATCH 17/52] shell: improve support for CLI options - Adds documentation - Adds a --help switch - Tidies short/long option form conventions --- docs/COMMAND_LINE.md | 20 +++++++++++++++ docs/tr1/CHANGELOG.md | 2 ++ docs/tr2/CHANGELOG.md | 2 ++ src/libtrx/game/shell/main.c | 8 +++--- src/libtrx/include/libtrx/game/shell.h | 2 +- src/tr1/game/output.c | 11 ++++++++ src/tr1/game/shell.c | 34 +++++++++++++++++++------ src/tr2/game/render/common.c | 2 -- src/tr2/game/shell/common.c | 35 ++++++++++++++++++++------ 9 files changed, 94 insertions(+), 22 deletions(-) create mode 100644 docs/COMMAND_LINE.md diff --git a/docs/COMMAND_LINE.md b/docs/COMMAND_LINE.md new file mode 100644 index 000000000..9fca2d295 --- /dev/null +++ b/docs/COMMAND_LINE.md @@ -0,0 +1,20 @@ +# Command line options + +Currently the following command line interface options are available: + +`-g/--gold` (legacy: `-gold`): + Runs the Unfinished Business or the Golden Mask expansion pack, depending + on the game. + +`--demo-pc` (TR1X only, legacy: `-demo_pc`): + Runs the PC demo level. + +`-l/--level `: + Runs the game immediately launching it into the specified level. + The path should be absolute. Internally, this option uses + `TR*X_gameflow_level.json5` as a template instructing it how to run the + game. + +`-s/--save `: + Runs the game immediately loading a specific save slot. The first save + starts at `num=1`. This option can be combined with `-l/--level`. diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index 6ca1234bd..7047d5433 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -6,6 +6,8 @@ - added support for aspect ratio-specific images (#1840) - added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) - changed the `draw_distance_fade` and `draw_distance_max` to `fog_start` and `fog_end` +- added aliases to CLI options (`-gold` becomes `-g/--gold`, `-demo_pc` becomes `--demo-pc`) +- added a `--help` CLI option (may not output anything on Windows machines – OS bug) - changed `Select Detail` dialog title to `Graphic Options` - changed the number of static mesh slots from 50 to 256 (#2734) - changed the "enable EIDOS logo" option to disable the Core Design and Bink Video Codec FMVs as well; renamed to "enable legal" (#2741) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index fa31d551c..11a28dcb9 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,4 +1,6 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...develop) - ××××-××-×× +- added aliases to CLI options (`-gold` becomes `-g/--gold`) +- added a `--help` CLI option (may not output anything on Windows machines – OS bug) - changed the sound dialog appearance (repositioned, added text labels and arrows) - fixed Lara voiding if she stops on a tile with a closing door, and the door isn't on a portal (#2848) - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) diff --git a/src/libtrx/game/shell/main.c b/src/libtrx/game/shell/main.c index 1a29106ab..4e3ce7d41 100644 --- a/src/libtrx/game/shell/main.c +++ b/src/libtrx/game/shell/main.c @@ -21,13 +21,11 @@ int main(int argc, char *argv[]) Log_Init(log_path); Memory_FreePointer(&log_path); - LOG_INFO("Game directory: %s", File_GetGameDirectory()); - m_ArgCount = argc; m_ArgStrings = (const char **)argv; Shell_Setup(); - Shell_Main(); - Shell_Terminate(0); - return 0; + int32_t exit_code = Shell_Main(); + Shell_Terminate(exit_code); + return exit_code; } diff --git a/src/libtrx/include/libtrx/game/shell.h b/src/libtrx/include/libtrx/game/shell.h index 3128bda38..e1cfc303a 100644 --- a/src/libtrx/include/libtrx/game/shell.h +++ b/src/libtrx/include/libtrx/game/shell.h @@ -12,7 +12,7 @@ extern void Shell_Shutdown(void); extern SDL_Window *Shell_GetWindow(void); void Shell_Setup(void); -extern void Shell_Main(void); +extern int32_t Shell_Main(void); void Shell_Terminate(int32_t exit_code); void Shell_ExitSystem(const char *message); void Shell_ExitSystemFmt(const char *fmt, ...); diff --git a/src/tr1/game/output.c b/src/tr1/game/output.c index 9bed2681b..c63c73c94 100644 --- a/src/tr1/game/output.c +++ b/src/tr1/game/output.c @@ -49,6 +49,7 @@ typedef struct { int32_t thickness; } LIGHTNING; +static bool m_Initialized = false; static int32_t m_LightningCount = 0; static LIGHTNING m_LightningTable[MAX_LIGHTNINGS]; static int32_t m_TextureMap[GFX_MAX_TEXTURES] = { GFX_NO_TEXTURE }; @@ -385,6 +386,11 @@ static void M_DrawSprite( bool Output_Init(void) { + if (m_Initialized) { + return true; + } + m_Initialized = true; + for (int32_t i = 0; i < GFX_MAX_TEXTURES; i++) { m_TextureMap[i] = GFX_NO_TEXTURE; m_TextureSurfaces[i] = nullptr; @@ -406,6 +412,11 @@ bool Output_Init(void) void Output_Shutdown(void) { + if (!m_Initialized) { + return; + } + m_Initialized = false; + Output_Meshes_Shutdown(); Output_Sprites_Shutdown(); Output_Textures_Shutdown(); diff --git a/src/tr1/game/shell.c b/src/tr1/game/shell.c index f892719ba..72910c037 100644 --- a/src/tr1/game/shell.c +++ b/src/tr1/game/shell.c @@ -81,11 +81,22 @@ static SHELL_ARGS m_Args = { static const char *m_CurrentGameFlowPath; -static void M_ParseArgs(SHELL_ARGS *out_args); +static void M_ShowHelp(void); +static bool M_ParseArgs(SHELL_ARGS *out_args); static void M_LoadConfig(void); static void M_HandleConfigChange(const EVENT *event, void *data); -static void M_ParseArgs(SHELL_ARGS *const out_args) +static void M_ShowHelp(void) +{ + puts("Currently available options:"); + puts(""); + puts("-g/--gold: launch The Unfinished Business expansion pack."); + puts(" --demo-pc: launch the PC demo level file."); + puts("-l/--level : launch a specific level file."); + puts("-s/--save : launch from a specific save slot (starts at 1)."); +} + +static bool M_ParseArgs(SHELL_ARGS *const out_args) { const char **args = nullptr; int32_t arg_count = 0; @@ -94,10 +105,15 @@ static void M_ParseArgs(SHELL_ARGS *const out_args) out_args->mod = M_MOD_OG; for (int32_t i = 0; i < arg_count; i++) { - if (!strcmp(args[i], "-gold")) { + if (!strcmp(args[i], "-h") || !strcmp(args[i], "--help")) { + M_ShowHelp(); + return false; + } + if (!strcmp(args[i], "-g") || !strcmp(args[i], "--gold") + || !strcmp(args[i], "-gold")) { out_args->mod = M_MOD_UB; } - if (!strcmp(args[i], "-demo_pc")) { + if (!strcmp(args[i], "--demo-pc") || !strcmp(args[i], "-demo_pc")) { out_args->mod = M_MOD_DEMO_PC; } if ((!strcmp(args[i], "-l") || !strcmp(args[i], "--level")) @@ -112,6 +128,7 @@ static void M_ParseArgs(SHELL_ARGS *const out_args) } } } + return true; } static void M_HandleConfigChange(const EVENT *const event, void *const data) @@ -174,9 +191,11 @@ const char *Shell_GetGameFlowPath(void) return m_ModPaths[m_Args.mod].game_flow_path; } -void Shell_Main(void) +int32_t Shell_Main(void) { - M_ParseArgs(&m_Args); + if (!M_ParseArgs(&m_Args)) { + return 0; + } GameString_Init(); EnumMap_Init(); @@ -200,7 +219,7 @@ void Shell_Main(void) if (!Output_Init()) { Shell_ExitSystem("Could not initialise video system"); - return; + return 1; } Screen_Init(); @@ -312,6 +331,7 @@ void Shell_Main(void) if (m_Args.level_to_play != nullptr) { Memory_FreePointer(&g_GameFlow.level_tables[GFLT_MAIN].levels[0].path); } + return 0; } void Shell_ProcessInput(void) diff --git a/src/tr2/game/render/common.c b/src/tr2/game/render/common.c index bcfb617ff..82b232443 100644 --- a/src/tr2/game/render/common.c +++ b/src/tr2/game/render/common.c @@ -45,7 +45,6 @@ static RENDERER *M_GetRenderer(void) } else if (g_Config.rendering.render_mode == RM_HARDWARE) { r = &m_Renderer_HW; } - ASSERT(r != nullptr); return r; } @@ -124,7 +123,6 @@ void Render_Init(void) void Render_Shutdown(void) { - LOG_DEBUG(""); RENDERER *const r = M_GetRenderer(); if (r != nullptr) { r->Close(r); diff --git a/src/tr2/game/shell/common.c b/src/tr2/game/shell/common.c index f7d00b73d..a1e87969b 100644 --- a/src/tr2/game/shell/common.c +++ b/src/tr2/game/shell/common.c @@ -99,7 +99,8 @@ static void M_HandleQuit(void); static void M_ConfigureOpenGL(void); static bool M_CreateGameWindow(void); -static void M_ParseArgs(SHELL_ARGS *out_args); +static void M_ShowHelp(void); +static bool M_ParseArgs(SHELL_ARGS *out_args); static void M_LoadConfig(void); static void M_HandleConfigChange(const EVENT *event, void *data); @@ -342,7 +343,16 @@ static bool M_CreateGameWindow(void) return true; } -static void M_ParseArgs(SHELL_ARGS *const out_args) +static void M_ShowHelp(void) +{ + puts("Currently available options:"); + puts(""); + puts("-g/--gold: launch The Golden Mask expansion pack."); + puts("-l/--level : launch a specific level file."); + puts("-s/--save : launch from a specific save slot (starts at 1)."); +} + +static bool M_ParseArgs(SHELL_ARGS *const out_args) { const char **args = nullptr; int32_t arg_count = 0; @@ -351,7 +361,12 @@ static void M_ParseArgs(SHELL_ARGS *const out_args) out_args->mod = M_MOD_OG; for (int32_t i = 0; i < arg_count; i++) { - if (!strcmp(args[i], "-gold")) { + if (!strcmp(args[i], "-h") || !strcmp(args[i], "--help")) { + M_ShowHelp(); + return false; + } + if (!strcmp(args[i], "-g") || !strcmp(args[i], "--gold") + || !strcmp(args[i], "-gold")) { out_args->mod = M_MOD_GM; } if ((!strcmp(args[i], "-l") || !strcmp(args[i], "--level")) @@ -366,6 +381,7 @@ static void M_ParseArgs(SHELL_ARGS *const out_args) } } } + return true; } static void M_LoadConfig(void) @@ -434,9 +450,13 @@ static void M_HandleConfigChange(const EVENT *const event, void *const data) } // TODO: refactor the hell out of me -void Shell_Main(void) +int32_t Shell_Main(void) { - M_ParseArgs(&m_Args); + if (!M_ParseArgs(&m_Args)) { + return 0; + } + + LOG_INFO("Game directory: %s", File_GetGameDirectory()); if (m_Args.mod == M_MOD_GM) { Object_Get(O_MONK_3)->setup_func = Monk3_Setup; @@ -465,7 +485,7 @@ void Shell_Main(void) if (!M_CreateGameWindow()) { Shell_ExitSystem("Failed to create game window"); - return; + return 1; } Random_Seed(); @@ -556,7 +576,7 @@ void Shell_Main(void) if (gf_cmd.action == GF_NOOP || gf_cmd.action == GF_EXIT_TO_TITLE) { Shell_ExitSystem("Title disabled & no replacement"); - return; + return 1; } } else { gf_cmd = GF_RunTitle(); @@ -578,6 +598,7 @@ void Shell_Main(void) if (m_Args.level_to_play != nullptr) { Memory_FreePointer(&g_GameFlow.level_tables[GFLT_MAIN].levels[0].path); } + return 0; } void Shell_Shutdown(void) From 73da4c294533d6ef4957f160c5d7a6d53c46cc82 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 14:07:20 +0200 Subject: [PATCH 18/52] shell: do not create logs with --help --- src/libtrx/game/shell/main.c | 16 ++---- src/libtrx/include/libtrx/game/shell.h | 2 +- src/tr1/game/shell.c | 72 ++++++++++++-------------- src/tr2/game/shell/common.c | 66 +++++++++++------------ 4 files changed, 66 insertions(+), 90 deletions(-) diff --git a/src/libtrx/game/shell/main.c b/src/libtrx/game/shell/main.c index 4e3ce7d41..54a5a0653 100644 --- a/src/libtrx/game/shell/main.c +++ b/src/libtrx/game/shell/main.c @@ -6,24 +6,16 @@ #include -static int m_ArgCount = 0; -static const char **m_ArgStrings = nullptr; - -void Shell_GetCommandLine(int *arg_count, const char ***args) -{ - *arg_count = m_ArgCount; - *args = m_ArgStrings; -} - int main(int argc, char *argv[]) { + if (!Shell_ParseArgs(argc, (const char **)argv)) { + return 0; + } + char *log_path = File_GetFullPath(PROJECT_NAME ".log"); Log_Init(log_path); Memory_FreePointer(&log_path); - m_ArgCount = argc; - m_ArgStrings = (const char **)argv; - Shell_Setup(); int32_t exit_code = Shell_Main(); Shell_Terminate(exit_code); diff --git a/src/libtrx/include/libtrx/game/shell.h b/src/libtrx/include/libtrx/game/shell.h index e1cfc303a..1ed37747f 100644 --- a/src/libtrx/include/libtrx/game/shell.h +++ b/src/libtrx/include/libtrx/game/shell.h @@ -11,13 +11,13 @@ typedef struct { extern void Shell_Shutdown(void); extern SDL_Window *Shell_GetWindow(void); +extern bool Shell_ParseArgs(int32_t arg_count, const char **args); void Shell_Setup(void); extern int32_t Shell_Main(void); void Shell_Terminate(int32_t exit_code); void Shell_ExitSystem(const char *message); void Shell_ExitSystemFmt(const char *fmt, ...); -void Shell_GetCommandLine(int *arg_count, const char ***args); void Shell_ScheduleExit(void); bool Shell_IsExiting(void); diff --git a/src/tr1/game/shell.c b/src/tr1/game/shell.c index 72910c037..718daeddc 100644 --- a/src/tr1/game/shell.c +++ b/src/tr1/game/shell.c @@ -82,7 +82,6 @@ static SHELL_ARGS m_Args = { static const char *m_CurrentGameFlowPath; static void M_ShowHelp(void); -static bool M_ParseArgs(SHELL_ARGS *out_args); static void M_LoadConfig(void); static void M_HandleConfigChange(const EVENT *event, void *data); @@ -96,41 +95,6 @@ static void M_ShowHelp(void) puts("-s/--save : launch from a specific save slot (starts at 1)."); } -static bool M_ParseArgs(SHELL_ARGS *const out_args) -{ - const char **args = nullptr; - int32_t arg_count = 0; - Shell_GetCommandLine(&arg_count, &args); - - out_args->mod = M_MOD_OG; - - for (int32_t i = 0; i < arg_count; i++) { - if (!strcmp(args[i], "-h") || !strcmp(args[i], "--help")) { - M_ShowHelp(); - return false; - } - if (!strcmp(args[i], "-g") || !strcmp(args[i], "--gold") - || !strcmp(args[i], "-gold")) { - out_args->mod = M_MOD_UB; - } - if (!strcmp(args[i], "--demo-pc") || !strcmp(args[i], "-demo_pc")) { - out_args->mod = M_MOD_DEMO_PC; - } - if ((!strcmp(args[i], "-l") || !strcmp(args[i], "--level")) - && i + 1 < arg_count) { - out_args->level_to_play = args[i + 1]; - out_args->mod = M_MOD_CUSTOM_LEVEL; - } - if ((!strcmp(args[i], "-s") || !strcmp(args[i], "--save")) - && i + 1 < arg_count) { - if (String_ParseInteger(args[i + 1], &out_args->save_to_load)) { - out_args->save_to_load--; - } - } - } - return true; -} - static void M_HandleConfigChange(const EVENT *const event, void *const data) { const CONFIG *const old = &g_Config; @@ -191,12 +155,40 @@ const char *Shell_GetGameFlowPath(void) return m_ModPaths[m_Args.mod].game_flow_path; } +bool Shell_ParseArgs(const int32_t arg_count, const char **args) +{ + SHELL_ARGS *const out_args = &m_Args; + out_args->mod = M_MOD_OG; + + for (int32_t i = 0; i < arg_count; i++) { + if (!strcmp(args[i], "-h") || !strcmp(args[i], "--help")) { + M_ShowHelp(); + return false; + } + if (!strcmp(args[i], "-g") || !strcmp(args[i], "--gold") + || !strcmp(args[i], "-gold")) { + out_args->mod = M_MOD_UB; + } + if (!strcmp(args[i], "--demo-pc") || !strcmp(args[i], "-demo_pc")) { + out_args->mod = M_MOD_DEMO_PC; + } + if ((!strcmp(args[i], "-l") || !strcmp(args[i], "--level")) + && i + 1 < arg_count) { + out_args->level_to_play = args[i + 1]; + out_args->mod = M_MOD_CUSTOM_LEVEL; + } + if ((!strcmp(args[i], "-s") || !strcmp(args[i], "--save")) + && i + 1 < arg_count) { + if (String_ParseInteger(args[i + 1], &out_args->save_to_load)) { + out_args->save_to_load--; + } + } + } + return true; +} + int32_t Shell_Main(void) { - if (!M_ParseArgs(&m_Args)) { - return 0; - } - GameString_Init(); EnumMap_Init(); Config_Init(); diff --git a/src/tr2/game/shell/common.c b/src/tr2/game/shell/common.c index a1e87969b..3935fca80 100644 --- a/src/tr2/game/shell/common.c +++ b/src/tr2/game/shell/common.c @@ -100,7 +100,6 @@ static void M_ConfigureOpenGL(void); static bool M_CreateGameWindow(void); static void M_ShowHelp(void); -static bool M_ParseArgs(SHELL_ARGS *out_args); static void M_LoadConfig(void); static void M_HandleConfigChange(const EVENT *event, void *data); @@ -352,38 +351,6 @@ static void M_ShowHelp(void) puts("-s/--save : launch from a specific save slot (starts at 1)."); } -static bool M_ParseArgs(SHELL_ARGS *const out_args) -{ - const char **args = nullptr; - int32_t arg_count = 0; - Shell_GetCommandLine(&arg_count, &args); - - out_args->mod = M_MOD_OG; - - for (int32_t i = 0; i < arg_count; i++) { - if (!strcmp(args[i], "-h") || !strcmp(args[i], "--help")) { - M_ShowHelp(); - return false; - } - if (!strcmp(args[i], "-g") || !strcmp(args[i], "--gold") - || !strcmp(args[i], "-gold")) { - out_args->mod = M_MOD_GM; - } - if ((!strcmp(args[i], "-l") || !strcmp(args[i], "--level")) - && i + 1 < arg_count) { - out_args->level_to_play = args[i + 1]; - out_args->mod = M_MOD_CUSTOM_LEVEL; - } - if ((!strcmp(args[i], "-s") || !strcmp(args[i], "--save")) - && i + 1 < arg_count) { - if (String_ParseInteger(args[i + 1], &out_args->save_to_load)) { - out_args->save_to_load--; - } - } - } - return true; -} - static void M_LoadConfig(void) { Config_Read(); @@ -449,13 +416,38 @@ static void M_HandleConfigChange(const EVENT *const event, void *const data) } } +bool Shell_ParseArgs(const int32_t arg_count, const char **args) +{ + SHELL_ARGS *const out_args = &m_Args; + out_args->mod = M_MOD_OG; + + for (int32_t i = 0; i < arg_count; i++) { + if (!strcmp(args[i], "-h") || !strcmp(args[i], "--help")) { + M_ShowHelp(); + return false; + } + if (!strcmp(args[i], "-g") || !strcmp(args[i], "--gold") + || !strcmp(args[i], "-gold")) { + out_args->mod = M_MOD_GM; + } + if ((!strcmp(args[i], "-l") || !strcmp(args[i], "--level")) + && i + 1 < arg_count) { + out_args->level_to_play = args[i + 1]; + out_args->mod = M_MOD_CUSTOM_LEVEL; + } + if ((!strcmp(args[i], "-s") || !strcmp(args[i], "--save")) + && i + 1 < arg_count) { + if (String_ParseInteger(args[i + 1], &out_args->save_to_load)) { + out_args->save_to_load--; + } + } + } + return true; +} + // TODO: refactor the hell out of me int32_t Shell_Main(void) { - if (!M_ParseArgs(&m_Args)) { - return 0; - } - LOG_INFO("Game directory: %s", File_GetGameDirectory()); if (m_Args.mod == M_MOD_GM) { From 92ff6e12c7a6a3995f76f448681f3f16af200561 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 11:23:46 +0200 Subject: [PATCH 19/52] game-strings: use OG JSON as a fallback in expansions Resolves #2847. --- docs/tr1/CHANGELOG.md | 1 + docs/tr2/CHANGELOG.md | 1 + src/libtrx/game/game_string_table/common.c | 52 ++++++++++++++++--- src/libtrx/game/game_string_table/priv.h | 5 +- src/libtrx/game/game_string_table/reader.c | 32 ++++-------- .../include/libtrx/game/game_string_table.h | 7 +-- src/tr1/game/shell.c | 8 ++- src/tr2/game/shell/common.c | 11 +++- 8 files changed, 81 insertions(+), 36 deletions(-) diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index 7047d5433..d7a272bab 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -14,6 +14,7 @@ - changed sprite pickups to respect the water tint if placed underwater (#2673) - changed save to take priority over load when both inputs are held on the same frame, in line with OG (#2833) - changed the sound dialog appearance (repositioned and added text labels) +- changed The Unfinished Business strings to default to the OG strings file for the main tables (#2847) - fixed the bilinear filter to not readjust the UVs (#2258) - fixed disabling the cutscenes causing the game to exit (#2743, regression from 4.8) - fixed anisotropy filter causing black lines on certain GPUs (#902) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 11a28dcb9..b897619f2 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...develop) - ××××-××-×× - added aliases to CLI options (`-gold` becomes `-g/--gold`) - added a `--help` CLI option (may not output anything on Windows machines – OS bug) +- changed The Golden Mask strings to default to the OG strings file for the main tables (#2847) - changed the sound dialog appearance (repositioned, added text labels and arrows) - fixed Lara voiding if she stops on a tile with a closing door, and the door isn't on a portal (#2848) - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) diff --git a/src/libtrx/game/game_string_table/common.c b/src/libtrx/game/game_string_table/common.c index d3f8327dc..9e40ae432 100644 --- a/src/libtrx/game/game_string_table/common.c +++ b/src/libtrx/game/game_string_table/common.c @@ -1,9 +1,11 @@ #include "debug.h" +#include "filesystem.h" #include "game/game_flow.h" #include "game/game_string.h" #include "game/game_string_table.h" #include "game/game_string_table/priv.h" #include "game/objects/names.h" +#include "game/shell.h" #include "log.h" #include "memory.h" @@ -11,8 +13,6 @@ typedef void (*M_LOAD_STRING_FUNC)(const char *, const char *); -GS_FILE g_GST_File = {}; - static struct { GAME_OBJECT_ID target_object_id; GAME_OBJECT_ID source_object_id; @@ -27,6 +27,8 @@ static struct { { .target_object_id = NO_OBJECT }, }; +static VECTOR *m_GST_Layers = nullptr; + static void M_Apply(const GS_TABLE *table); static void M_ApplyLevelTitles( const GS_FILE *gs_file, GF_LEVEL_TABLE_TYPE level_table_type); @@ -90,17 +92,19 @@ static void M_ApplyLevelTitles( GF_GetLevelTable(level_table_type); const GS_LEVEL_TABLE *const gs_level_table = &gs_file->level_tables[level_table_type]; + if (gs_level_table->count == 0) { + return; + } + ASSERT(gs_level_table->count == level_table->count); for (int32_t i = 0; i < level_table->count; i++) { GF_SetLevelTitle( &level_table->levels[i], gs_level_table->entries[i].title); } } -void GameStringTable_Apply(const GF_LEVEL *const level) +static void M_ApplyLayer( + const GF_LEVEL *const level, const GS_FILE *const gs_file) { - const GS_FILE *const gs_file = &g_GST_File; - - Object_ResetNames(); M_Apply(&gs_file->global); for (int32_t i = 0; i < GFLT_NUMBER_OF; i++) { @@ -126,10 +130,44 @@ void GameStringTable_Apply(const GF_LEVEL *const level) M_Apply(&gs_level_table->entries[level->num].table); } } +} + +void GameStringTable_Apply(const GF_LEVEL *const level) +{ + Object_ResetNames(); + ASSERT(m_GST_Layers != nullptr); + for (int32_t i = 0; i < m_GST_Layers->count; i++) { + const GS_FILE *const gs_file = Vector_Get(m_GST_Layers, i); + M_ApplyLayer(level, gs_file); + } M_DoObjectAliases(); } +void GameStringTable_Init(void) +{ + m_GST_Layers = Vector_Create(sizeof(GS_FILE)); +} + void GameStringTable_Shutdown(void) { - GS_File_Free(&g_GST_File); + if (m_GST_Layers != nullptr) { + for (int32_t i = 0; i < m_GST_Layers->count; i++) { + GS_FILE *const gs_file = Vector_Get(m_GST_Layers, i); + GS_File_Free(gs_file); + } + Vector_Free(m_GST_Layers); + m_GST_Layers = nullptr; + } +} + +void GameStringTable_Load(const char *const path, const bool load_levels) +{ + char *data = nullptr; + if (!File_Load(path, &data, nullptr)) { + Shell_ExitSystemFmt("failed to open strings file (path: %d)", path); + } + GS_FILE *gs_file = GS_File_CreateFromString(data, load_levels); + ASSERT(m_GST_Layers != nullptr); + Vector_Add(m_GST_Layers, gs_file); + Memory_FreePointer(&data); } diff --git a/src/libtrx/game/game_string_table/priv.h b/src/libtrx/game/game_string_table/priv.h index 00d69b7f0..9533c5989 100644 --- a/src/libtrx/game/game_string_table/priv.h +++ b/src/libtrx/game/game_string_table/priv.h @@ -1,6 +1,7 @@ #pragma once #include "game/game_flow/enum.h" +#include "vector.h" #include @@ -35,7 +36,7 @@ typedef struct { GS_LEVEL_TABLE level_tables[GFLT_NUMBER_OF]; } GS_FILE; -extern GS_FILE g_GST_File; - void GS_Table_Free(GS_TABLE *gs_table); + +GS_FILE *GS_File_CreateFromString(const char *data, bool load_levels); void GS_File_Free(GS_FILE *gs_file); diff --git a/src/libtrx/game/game_string_table/reader.c b/src/libtrx/game/game_string_table/reader.c index 4cec681d4..5ded75216 100644 --- a/src/libtrx/game/game_string_table/reader.c +++ b/src/libtrx/game/game_string_table/reader.c @@ -1,4 +1,3 @@ -#include "filesystem.h" #include "game/game_flow.h" #include "game/game_string_table.h" #include "game/game_string_table/priv.h" @@ -130,24 +129,14 @@ static void M_LoadLevelsFromJSON( } } -void GameStringTable_LoadFromFile(const char *const path) +GS_FILE *GS_File_CreateFromString( + const char *const data, const bool load_levels) { - char *data = nullptr; - if (!File_Load(path, &data, nullptr)) { - Shell_ExitSystemFmt("failed to open strings file (path: %d)", path); - } - GameStringTable_LoadFromString(data); - Memory_FreePointer(&data); -} - -void GameStringTable_LoadFromString(const char *const data) -{ - GameStringTable_Shutdown(); - - JSON_VALUE *root = nullptr; + GS_FILE *const gs_file = Memory_Alloc(sizeof(GS_FILE)); JSON_PARSE_RESULT parse_result; - root = JSON_ParseEx( + + JSON_VALUE *root = JSON_ParseEx( data, strlen(data), JSON_PARSE_FLAGS_ALLOW_JSON5, nullptr, nullptr, &parse_result); if (root == nullptr) { @@ -157,15 +146,16 @@ void GameStringTable_LoadFromString(const char *const data) parse_result.error_line_no, parse_result.error_row_no, data); } - GS_FILE *const gs_file = &g_GST_File; JSON_OBJECT *root_obj = JSON_ValueAsObject(root); M_LoadTableFromJSON(root_obj, &gs_file->global); - M_LoadLevelsFromJSON(root_obj, gs_file, "levels", GFLT_MAIN); - M_LoadLevelsFromJSON(root_obj, gs_file, "demos", GFLT_DEMOS); - M_LoadLevelsFromJSON(root_obj, gs_file, "cutscenes", GFLT_CUTSCENES); - + if (load_levels) { + M_LoadLevelsFromJSON(root_obj, gs_file, "levels", GFLT_MAIN); + M_LoadLevelsFromJSON(root_obj, gs_file, "demos", GFLT_DEMOS); + M_LoadLevelsFromJSON(root_obj, gs_file, "cutscenes", GFLT_CUTSCENES); + } if (root != nullptr) { JSON_ValueFree(root); root = nullptr; } + return gs_file; } diff --git a/src/libtrx/include/libtrx/game/game_string_table.h b/src/libtrx/include/libtrx/game/game_string_table.h index b14a99c97..cc33f3fe4 100644 --- a/src/libtrx/include/libtrx/game/game_string_table.h +++ b/src/libtrx/include/libtrx/game/game_string_table.h @@ -2,7 +2,8 @@ #include -void GameStringTable_LoadFromFile(const char *path); -void GameStringTable_LoadFromString(const char *data); -void GameStringTable_Apply(const GF_LEVEL *level); +void GameStringTable_Init(void); void GameStringTable_Shutdown(void); + +void GameStringTable_Load(const char *path, bool load_levels); +void GameStringTable_Apply(const GF_LEVEL *level); diff --git a/src/tr1/game/shell.c b/src/tr1/game/shell.c index 718daeddc..da1fdea06 100644 --- a/src/tr1/game/shell.c +++ b/src/tr1/game/shell.c @@ -133,6 +133,8 @@ void Shell_Shutdown(void) Console_Shutdown(); GameBuf_Shutdown(); Savegame_Shutdown(); + + GameStringTable_Shutdown(); GF_Shutdown(); Output_Shutdown(); @@ -217,7 +219,11 @@ int32_t Shell_Main(void) GF_Init(); GF_LoadFromFile(m_ModPaths[m_Args.mod].game_flow_path); - GameStringTable_LoadFromFile(m_ModPaths[m_Args.mod].game_strings_path); + GameStringTable_Init(); + if (m_Args.mod != M_MOD_OG) { + GameStringTable_Load(m_ModPaths[M_MOD_OG].game_strings_path, false); + } + GameStringTable_Load(m_ModPaths[m_Args.mod].game_strings_path, true); GameStringTable_Apply(nullptr); Savegame_Init(); diff --git a/src/tr2/game/shell/common.c b/src/tr2/game/shell/common.c index 3935fca80..b40b7e758 100644 --- a/src/tr2/game/shell/common.c +++ b/src/tr2/game/shell/common.c @@ -490,7 +490,12 @@ int32_t Shell_Main(void) GF_Init(); GF_LoadFromFile(m_ModPaths[m_Args.mod].game_flow_path); - GameStringTable_LoadFromFile(m_ModPaths[m_Args.mod].game_strings_path); + + GameStringTable_Init(); + if (m_Args.mod != M_MOD_OG) { + GameStringTable_Load(m_ModPaths[M_MOD_OG].game_strings_path, false); + } + GameStringTable_Load(m_ModPaths[m_Args.mod].game_strings_path, true); GameStringTable_Apply(nullptr); GameBuf_Init(); @@ -595,8 +600,9 @@ int32_t Shell_Main(void) void Shell_Shutdown(void) { + GameStringTable_Shutdown(); GF_Shutdown(); - GameString_Shutdown(); + Console_Shutdown(); Render_Shutdown(); Text_Shutdown(); @@ -604,6 +610,7 @@ void Shell_Shutdown(void) GameBuf_Shutdown(); Config_Shutdown(); EnumMap_Shutdown(); + GameString_Shutdown(); } const char *Shell_GetConfigPath(void) From e32a4c270f26946b70505a3625a7f46bb91e89d5 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 10:05:27 +0200 Subject: [PATCH 20/52] docs: fix formatting and mistyped option name --- docs/MIGRATING.md | 5 +++-- docs/tr1/CHANGELOG.md | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/MIGRATING.md b/docs/MIGRATING.md index aa61e581f..1c9f2deb8 100644 --- a/docs/MIGRATING.md +++ b/docs/MIGRATING.md @@ -6,10 +6,11 @@ 1. **Update fog configuration** If you wish to force your fog settings on player: - - Rename `draw_distance_min` to `fog_start` + - Rename `draw_distance_fade` to `fog_start` - Rename `draw_distance_max` to `fog_end` + If you wish to give the player agency to change the fog: - - Remove `draw_distance_min` and `draw_distance_max` + - Remove `draw_distance_fade` and `draw_distance_max` ### Version 4.7 to 4.8 diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index 818130279..4121e32f7 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -5,7 +5,7 @@ - added support for antitriggers, like TR2+ (#2580) - added support for aspect ratio-specific images (#1840) - added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) -- changed the `draw_distance_min` and `draw_distance_max` to `fog_start` and `fog_end` +- changed the `draw_distance_fade` and `draw_distance_max` to `fog_start` and `fog_end` - changed `Select Detail` dialog title to `Graphic Options` - changed the number of static mesh slots from 50 to 256 (#2734) - changed the "enable EIDOS logo" option to disable the Core Design and Bink Video Codec FMVs as well; renamed to "enable legal" (#2741) From 4035fe64113613b7e3afe9fd843b7d044559f204 Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Sat, 26 Apr 2025 09:57:06 +0100 Subject: [PATCH 21/52] items: replace items by index rather than room Carried items use NO_ROOM so were not included when replacing guns with ammo. This ensures everything is checked when replacing IDs. Resolves #2850. Resolves #2856. --- docs/tr2/CHANGELOG.md | 2 ++ docs/tr2/README.md | 1 + src/libtrx/game/items.c | 14 +++++--------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 1577ff805..fbb4edb46 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,4 +1,6 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...develop) - ××××-××-×× +- fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) +- fixed guns carried by enemies not being converted to ammo if Lara starts the level with the gun and the game has later been reloaded (#2850, regression from 1.0) ## [1.0.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...tr2-1.0.1) - 2025-04-24 - added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) diff --git a/docs/tr2/README.md b/docs/tr2/README.md index 9faec4fd4..d0fe8a2f2 100644 --- a/docs/tr2/README.md +++ b/docs/tr2/README.md @@ -254,6 +254,7 @@ However, you can easily download them manually from these urls: - fixed Floating Islands mystic plaque inventory rotation - fixed pushblocks being rotated when Lara grabs them, most noticeable if asymmetric textures have been used - fixed being able to use hotkeys in the end-level statistics screen +- fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level - fixed destroyed gondolas appearing embedded in the ground after loading a save - improved the animation of Lara's braid diff --git a/src/libtrx/game/items.c b/src/libtrx/game/items.c index a2698b37b..f92b67299 100644 --- a/src/libtrx/game/items.c +++ b/src/libtrx/game/items.c @@ -219,15 +219,11 @@ int32_t Item_GlobalReplace( { int32_t changed = 0; - for (int32_t i = 0; i < Room_GetCount(); i++) { - int16_t item_num = Room_Get(i)->item_num; - while (item_num != NO_ITEM) { - ITEM *const item = &m_Items[item_num]; - if (item->object_id == src_obj_id) { - item->object_id = dst_obj_id; - changed++; - } - item_num = item->next_item; + for (int32_t item_num = 0; item_num < m_MaxUsedItemCount; item_num++) { + ITEM *const item = &m_Items[item_num]; + if (item->object_id == src_obj_id) { + item->object_id = dst_obj_id; + changed++; } } From 2f2f0c6842ff800b9685b1d732fb1393b3dcacfa Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 11:55:23 +0200 Subject: [PATCH 22/52] tr2/viewport: fix screenshots at wrong resolution Resolves #2845. --- docs/tr2/CHANGELOG.md | 1 + src/tr2/game/viewport.c | 11 +++++++---- src/tr2/game/viewport.h | 5 ++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index fbb4edb46..9362fd2ce 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...develop) - ××××-××-×× - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) - fixed guns carried by enemies not being converted to ammo if Lara starts the level with the gun and the game has later been reloaded (#2850, regression from 1.0) +- fixed 1920x1080 screenshots in 16:9 aspect mode being saved as 1919x1080 (#2845, regression from 0.8) ## [1.0.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...tr2-1.0.1) - 2025-04-24 - added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) diff --git a/src/tr2/game/viewport.c b/src/tr2/game/viewport.c index 245e1c11c..424039f73 100644 --- a/src/tr2/game/viewport.c +++ b/src/tr2/game/viewport.c @@ -125,20 +125,23 @@ void Viewport_Reset(void) VIEWPORT *const vp = &m_Viewport; switch (g_Config.rendering.aspect_mode) { case AM_4_3: - vp->render_ar = 4.0 / 3.0; + vp->render_ar.w = 4; + vp->render_ar.h = 3; break; case AM_16_9: - vp->render_ar = 16.0 / 9.0; + vp->render_ar.w = 16; + vp->render_ar.h = 9; break; case AM_ANY: - vp->render_ar = size.w / (double)size.h; + vp->render_ar.w = size.w; + vp->render_ar.h = size.h; break; } vp->width = size.w / g_Config.rendering.scaler; vp->height = size.h / g_Config.rendering.scaler; if (g_Config.rendering.aspect_mode != AM_ANY) { - vp->width = vp->height * vp->render_ar; + vp->width = vp->height * vp->render_ar.w / vp->render_ar.h; } vp->near_z = Output_GetNearZ() >> W2V_SHIFT; diff --git a/src/tr2/game/viewport.h b/src/tr2/game/viewport.h index 9af7d0105..8ec3d7485 100644 --- a/src/tr2/game/viewport.h +++ b/src/tr2/game/viewport.h @@ -8,7 +8,10 @@ typedef struct { int32_t near_z; int32_t far_z; int16_t view_angle; - double render_ar; + struct { + int32_t w; + int32_t h; + } render_ar; // TODO: remove most of these variables if possible struct { From d6fc167749d18f2cce17904686e3d4561e6c909c Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Sat, 26 Apr 2025 10:20:06 +0100 Subject: [PATCH 23/52] tr2/objects/door: prevent Lara voiding in closed doors This uses the same approach as TR1 to avoid Lara voiding in closing/ closed doors that are not placed on portals. Resolves #2848. --- docs/tr2/CHANGELOG.md | 1 + docs/tr2/README.md | 1 + src/tr2/game/objects/general/door.c | 59 ++++++++++++++++++++++------- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 9362fd2ce..ad9328daf 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,4 +1,5 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...develop) - ××××-××-×× +- fixed Lara voiding if she stops on a tile with a closing door, and the door isn't on a portal (#2848) - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) - fixed guns carried by enemies not being converted to ammo if Lara starts the level with the gun and the game has later been reloaded (#2850, regression from 1.0) - fixed 1920x1080 screenshots in 16:9 aspect mode being saved as 1919x1080 (#2845, regression from 0.8) diff --git a/docs/tr2/README.md b/docs/tr2/README.md index d0fe8a2f2..54d53df6a 100644 --- a/docs/tr2/README.md +++ b/docs/tr2/README.md @@ -256,6 +256,7 @@ However, you can easily download them manually from these urls: - fixed being able to use hotkeys in the end-level statistics screen - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level - fixed destroyed gondolas appearing embedded in the ground after loading a save +- fixed Lara voiding if she stops on a tile with a closing door, and the door isn't on a portal - improved the animation of Lara's braid #### Cheats diff --git a/src/tr2/game/objects/general/door.c b/src/tr2/game/objects/general/door.c index 819c8fb5f..e97ec0cd5 100644 --- a/src/tr2/game/objects/general/door.c +++ b/src/tr2/game/objects/general/door.c @@ -21,6 +21,8 @@ static SECTOR *M_GetRoomRelSector( static void M_InitialisePortal( const ROOM *room, const ITEM *item, int32_t sector_dx, int32_t sector_dz, DOORPOS_DATA *door_pos); +static bool M_LaraDoorCollision(const SECTOR *sector); +static void M_Check(DOORPOS_DATA *d); static void M_Shut(DOORPOS_DATA *d); static void M_Open(DOORPOS_DATA *d); static void M_Setup(OBJECT *obj); @@ -38,7 +40,32 @@ static SECTOR *M_GetRoomRelSector( return Room_GetUnitSector(room, sector.x, sector.z); } -static void Door_Shut(DOORPOS_DATA *const d) +static bool M_LaraDoorCollision(const SECTOR *const sector) +{ + // Check if Lara is on the same tile as the invisible block. + const ITEM *const lara = Lara_GetItem(); + if (lara == nullptr) { + return false; + } + + int16_t room_num = lara->room_num; + const SECTOR *const lara_sector = + Room_GetSector(lara->pos.x, lara->pos.y, lara->pos.z, &room_num); + return lara_sector == sector; +} + +static void M_Check(DOORPOS_DATA *const d) +{ + // Forcefully remove the invisible block if Lara happens to occupy the same + // tile. This ensures that Lara doesn't void if a timed door happens to + // close right on her, or the player loads the game while standing on a + // closed door's block tile. + if (M_LaraDoorCollision(d->sector)) { + M_Open(d); + } +} + +static void M_Shut(DOORPOS_DATA *const d) { SECTOR *const sector = d->sector; if (d->sector == nullptr) { @@ -61,7 +88,7 @@ static void Door_Shut(DOORPOS_DATA *const d) } } -static void Door_Open(DOORPOS_DATA *const d) +static void M_Open(DOORPOS_DATA *const d) { if (d->sector == nullptr) { return; @@ -137,8 +164,8 @@ static void M_Initialise(const int16_t item_num) } room_num = door->d1.sector->portal_room.wall; - Door_Shut(&door->d1); - Door_Shut(&door->d1flip); + M_Shut(&door->d1); + M_Shut(&door->d1flip); if (room_num == NO_ROOM) { door->d2.sector = nullptr; @@ -153,8 +180,8 @@ static void M_Initialise(const int16_t item_num) M_InitialisePortal(room, item, 0, 0, &door->d2flip); } - Door_Shut(&door->d2); - Door_Shut(&door->d2flip); + M_Shut(&door->d2); + M_Shut(&door->d2flip); const int16_t prev_room = item->room_num; Item_NewRoom(item_num, room_num); @@ -171,22 +198,26 @@ static void M_Control(const int16_t item_num) if (item->current_anim_state == DOOR_STATE_CLOSED) { item->goal_anim_state = DOOR_STATE_OPEN; } else { - Door_Open(&data->d1); - Door_Open(&data->d2); - Door_Open(&data->d1flip); - Door_Open(&data->d2flip); + M_Open(&data->d1); + M_Open(&data->d2); + M_Open(&data->d1flip); + M_Open(&data->d2flip); } } else { if (item->current_anim_state == DOOR_STATE_OPEN) { item->goal_anim_state = DOOR_STATE_CLOSED; } else { - Door_Shut(&data->d1); - Door_Shut(&data->d2); - Door_Shut(&data->d1flip); - Door_Shut(&data->d2flip); + M_Shut(&data->d1); + M_Shut(&data->d2); + M_Shut(&data->d1flip); + M_Shut(&data->d2flip); } } + M_Check(&data->d1); + M_Check(&data->d2); + M_Check(&data->d1flip); + M_Check(&data->d2flip); Item_Animate(item); } From 5a50de02ed1cbf5de02c4bf59765a90a4dd4ebfa Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Sat, 26 Apr 2025 10:21:03 +0100 Subject: [PATCH 24/52] door: move door module to TRX This moves the door module fully to TRX as the logic is identical in both games. --- .../game/objects/general/door.c | 60 +++-- .../include/libtrx/game/objects/common.h | 1 + src/libtrx/meson.build | 1 + src/tr1/game/objects/common.h | 1 - src/tr1/game/objects/general/door.c | 253 ------------------ src/tr1/global/types.h | 6 - src/tr1/meson.build | 1 - src/tr2/game/objects/common.h | 1 - src/tr2/global/types_decomp.h | 6 - src/tr2/meson.build | 1 - 10 files changed, 34 insertions(+), 297 deletions(-) rename src/{tr2 => libtrx}/game/objects/general/door.c (86%) delete mode 100644 src/tr1/game/objects/general/door.c diff --git a/src/tr2/game/objects/general/door.c b/src/libtrx/game/objects/general/door.c similarity index 86% rename from src/tr2/game/objects/general/door.c rename to src/libtrx/game/objects/general/door.c index e97ec0cd5..e81c4ca39 100644 --- a/src/tr2/game/objects/general/door.c +++ b/src/libtrx/game/objects/general/door.c @@ -1,13 +1,16 @@ -#include "game/box.h" -#include "game/items.h" -#include "game/objects/common.h" -#include "game/room.h" -#include "global/vars.h" +#include "game/objects/general/door.h" -#include -#include -#include -#include +#include "game/game_buf.h" +#include "game/lara/common.h" +#include "game/objects/common.h" +#include "game/pathing.h" +#include "game/rooms.h" + +typedef struct { + SECTOR *sector; + SECTOR old_sector; + int16_t box_num; +} DOORPOS_DATA; typedef struct { DOORPOS_DATA d1; @@ -82,7 +85,7 @@ static void M_Shut(DOORPOS_DATA *const d) sector->portal_room.pit = NO_ROOM_NEG; sector->portal_room.wall = NO_ROOM; - const int16_t box_num = d->block; + const int16_t box_num = d->box_num; if (box_num != NO_BOX) { Box_GetBox(box_num)->overlap_index |= BOX_BLOCKED; } @@ -96,7 +99,7 @@ static void M_Open(DOORPOS_DATA *const d) *d->sector = d->old_sector; - const int16_t box_num = d->block; + const int16_t box_num = d->box_num; if (box_num != NO_BOX) { Box_GetBox(box_num)->overlap_index &= ~BOX_BLOCKED; } @@ -117,10 +120,11 @@ static void M_InitialisePortal( } int16_t box_num = sector->box; - if (!(Box_GetBox(box_num)->overlap_index & BOX_BLOCKABLE)) { + const BOX_INFO *const box = Box_GetBox(box_num); + if ((box->overlap_index & BOX_BLOCKABLE) == 0) { box_num = NO_BOX; } - door_pos->block = box_num; + door_pos->box_num = box_num; door_pos->old_sector = *door_pos->sector; } @@ -192,32 +196,32 @@ static void M_Initialise(const int16_t item_num) static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); - DOOR_DATA *const data = item->data; + DOOR_DATA *const door = item->data; if (Item_IsTriggerActive(item)) { if (item->current_anim_state == DOOR_STATE_CLOSED) { item->goal_anim_state = DOOR_STATE_OPEN; } else { - M_Open(&data->d1); - M_Open(&data->d2); - M_Open(&data->d1flip); - M_Open(&data->d2flip); + M_Open(&door->d1); + M_Open(&door->d2); + M_Open(&door->d1flip); + M_Open(&door->d2flip); } } else { if (item->current_anim_state == DOOR_STATE_OPEN) { item->goal_anim_state = DOOR_STATE_CLOSED; } else { - M_Shut(&data->d1); - M_Shut(&data->d2); - M_Shut(&data->d1flip); - M_Shut(&data->d2flip); + M_Shut(&door->d1); + M_Shut(&door->d2); + M_Shut(&door->d1flip); + M_Shut(&door->d2flip); } } - M_Check(&data->d1); - M_Check(&data->d2); - M_Check(&data->d1flip); - M_Check(&data->d2flip); + M_Check(&door->d1); + M_Check(&door->d2); + M_Check(&door->d1flip); + M_Check(&door->d2flip); Item_Animate(item); } @@ -237,8 +241,8 @@ void Door_Collision( if (coll->enable_baddie_push) { Lara_Push( item, coll, - item->current_anim_state != item->goal_anim_state ? coll->enable_hit - : false, + coll->enable_hit + && item->current_anim_state != item->goal_anim_state, true); } } diff --git a/src/libtrx/include/libtrx/game/objects/common.h b/src/libtrx/include/libtrx/game/objects/common.h index f65578f14..18de887cd 100644 --- a/src/libtrx/include/libtrx/game/objects/common.h +++ b/src/libtrx/include/libtrx/game/objects/common.h @@ -36,6 +36,7 @@ void Object_SwapMesh( ANIM *Object_GetAnim(const OBJECT *obj, int32_t anim_idx); ANIM_BONE *Object_GetBone(const OBJECT *obj, int32_t bone_idx); +extern void Object_DrawUnclippedItem(const ITEM *item); extern void Object_DrawMesh(int32_t mesh_idx, int32_t clip, bool interpolated); void Object_DrawInterpolatedObject( diff --git a/src/libtrx/meson.build b/src/libtrx/meson.build index d3866bca2..78793400b 100644 --- a/src/libtrx/meson.build +++ b/src/libtrx/meson.build @@ -176,6 +176,7 @@ sources = [ 'game/objects/general/bridge_flat.c', 'game/objects/general/bridge_tilt1.c', 'game/objects/general/bridge_tilt2.c', + 'game/objects/general/door.c', 'game/objects/general/drawbridge.c', 'game/objects/general/trapdoor.c', 'game/objects/names.c', diff --git a/src/tr1/game/objects/common.h b/src/tr1/game/objects/common.h index dd39f97e8..5b9da9fdc 100644 --- a/src/tr1/game/objects/common.h +++ b/src/tr1/game/objects/common.h @@ -12,7 +12,6 @@ void Object_DrawDummyItem(const ITEM *item); void Object_DrawSpriteItem(const ITEM *item); void Object_DrawPickupItem(const ITEM *item); void Object_DrawAnimatingItem(const ITEM *item); -void Object_DrawUnclippedItem(const ITEM *item); void Object_SetMeshReflective( GAME_OBJECT_ID obj_id, int32_t mesh_idx, bool enabled); void Object_SetReflective(GAME_OBJECT_ID obj_id, bool enabled); diff --git a/src/tr1/game/objects/general/door.c b/src/tr1/game/objects/general/door.c deleted file mode 100644 index 5cbe0bfcc..000000000 --- a/src/tr1/game/objects/general/door.c +++ /dev/null @@ -1,253 +0,0 @@ -#include "game/box.h" -#include "game/items.h" -#include "game/lara/common.h" -#include "game/objects/common.h" -#include "game/room.h" -#include "global/vars.h" - -#include -#include -#include -#include - -typedef struct { - DOORPOS_DATA d1; - DOORPOS_DATA d1flip; - DOORPOS_DATA d2; - DOORPOS_DATA d2flip; -} DOOR_DATA; - -static SECTOR *M_GetRoomRelSector( - const ROOM *room, const ITEM *item, int32_t sector_dx, int32_t sector_dz); -static void M_InitialisePortal( - const ROOM *room, const ITEM *item, int32_t sector_dx, int32_t sector_dz, - DOORPOS_DATA *door_pos); - -static bool M_LaraDoorCollision(const SECTOR *sector); -static void M_Check(DOORPOS_DATA *d); -static void M_Shut(DOORPOS_DATA *d); -static void M_Open(DOORPOS_DATA *d); -static void M_Setup(OBJECT *obj); -static void M_Initialise(int16_t item_num); -static void M_Control(int16_t item_num); - -static SECTOR *M_GetRoomRelSector( - const ROOM *const room, const ITEM *item, const int32_t sector_dx, - const int32_t sector_dz) -{ - const XZ_32 sector = { - .x = ((item->pos.x - room->pos.x) >> WALL_SHIFT) + sector_dx, - .z = ((item->pos.z - room->pos.z) >> WALL_SHIFT) + sector_dz, - }; - return Room_GetUnitSector(room, sector.x, sector.z); -} - -static void M_InitialisePortal( - const ROOM *const room, const ITEM *const item, const int32_t sector_dx, - const int32_t sector_dz, DOORPOS_DATA *const door_pos) -{ - door_pos->sector = M_GetRoomRelSector(room, item, sector_dx, sector_dz); - - const SECTOR *sector = door_pos->sector; - - const int16_t room_num = sector->portal_room.wall; - if (room_num != NO_ROOM) { - sector = - M_GetRoomRelSector(Room_Get(room_num), item, sector_dx, sector_dz); - } - - int16_t box_num = sector->box; - if (!(Box_GetBox(box_num)->overlap_index & BOX_BLOCKABLE)) { - box_num = NO_BOX; - } - door_pos->block = box_num; - door_pos->old_sector = *door_pos->sector; -} - -static bool M_LaraDoorCollision(const SECTOR *const sector) -{ - // Check if Lara is on the same tile as the invisible block. - if (g_LaraItem == nullptr) { - return false; - } - - int16_t room_num = g_LaraItem->room_num; - const SECTOR *const lara_sector = Room_GetSector( - g_LaraItem->pos.x, g_LaraItem->pos.y, g_LaraItem->pos.z, &room_num); - return lara_sector == sector; -} - -static void M_Check(DOORPOS_DATA *const d) -{ - // Forcefully remove the invisible block if Lara happens to occupy the same - // tile. This ensures that Lara doesn't void if a timed door happens to - // close right on her, or the player loads the game while standing on a - // closed door's block tile. - if (M_LaraDoorCollision(d->sector)) { - M_Open(d); - } -} - -static void M_Shut(DOORPOS_DATA *const d) -{ - // Change the level geometry so that the door tile is impassable. - SECTOR *const sector = d->sector; - if (sector == nullptr) { - return; - } - - sector->box = NO_BOX; - sector->floor.height = NO_HEIGHT; - sector->ceiling.height = NO_HEIGHT; - sector->floor.tilt = 0; - sector->ceiling.tilt = 0; - sector->portal_room.sky = NO_ROOM; - sector->portal_room.pit = NO_ROOM; - sector->portal_room.wall = NO_ROOM; - - const int16_t box_num = d->block; - if (box_num != NO_BOX) { - Box_GetBox(box_num)->overlap_index |= BOX_BLOCKED; - } -} - -static void M_Open(DOORPOS_DATA *const d) -{ - // Restore the level geometry so that the door tile is passable. - SECTOR *const sector = d->sector; - if (!sector) { - return; - } - - *sector = d->old_sector; - - const int16_t box_num = d->block; - if (box_num != NO_BOX) { - Box_GetBox(box_num)->overlap_index &= ~BOX_BLOCKED; - } -} - -static void M_Setup(OBJECT *const obj) -{ - obj->initialise_func = M_Initialise; - obj->control_func = M_Control; - obj->draw_func = Object_DrawUnclippedItem; - obj->collision_func = Door_Collision; - obj->save_anim = 1; - obj->save_flags = 1; -} - -static void M_Initialise(const int16_t item_num) -{ - ITEM *const item = Item_Get(item_num); - DOOR_DATA *const door = GameBuf_Alloc(sizeof(DOOR_DATA), GBUF_ITEM_DATA); - item->data = door; - - int32_t dx = 0; - int32_t dz = 0; - if (item->rot.y == 0) { - dz = -1; - } else if (item->rot.y == -DEG_180) { - dz = 1; - } else if (item->rot.y == DEG_90) { - dx = -1; - } else { - dx = 1; - } - - int16_t room_num = item->room_num; - const ROOM *room = Room_Get(room_num); - M_InitialisePortal(room, item, dx, dz, &door->d1); - - if (room->flipped_room == -1) { - door->d1flip.sector = nullptr; - } else { - room = Room_Get(room->flipped_room); - M_InitialisePortal(room, item, dx, dz, &door->d1flip); - } - - room_num = door->d1.sector->portal_room.wall; - M_Shut(&door->d1); - M_Shut(&door->d1flip); - - if (room_num == NO_ROOM) { - door->d2.sector = nullptr; - door->d2flip.sector = nullptr; - return; - } - - room = Room_Get(room_num); - M_InitialisePortal(room, item, 0, 0, &door->d2); - if (room->flipped_room == -1) { - door->d2flip.sector = nullptr; - } else { - room = Room_Get(room->flipped_room); - M_InitialisePortal(room, item, 0, 0, &door->d2flip); - } - - M_Shut(&door->d2); - M_Shut(&door->d2flip); - - const int16_t prev_room = item->room_num; - Item_NewRoom(item_num, room_num); - item->room_num = prev_room; -} - -static void M_Control(const int16_t item_num) -{ - ITEM *const item = Item_Get(item_num); - DOOR_DATA *door = item->data; - - if (Item_IsTriggerActive(item)) { - if (item->current_anim_state == DOOR_STATE_CLOSED) { - item->goal_anim_state = DOOR_STATE_OPEN; - } else { - M_Open(&door->d1); - M_Open(&door->d2); - M_Open(&door->d1flip); - M_Open(&door->d2flip); - } - } else { - if (item->current_anim_state == DOOR_STATE_OPEN) { - item->goal_anim_state = DOOR_STATE_CLOSED; - } else { - M_Shut(&door->d1); - M_Shut(&door->d2); - M_Shut(&door->d1flip); - M_Shut(&door->d2flip); - } - } - - M_Check(&door->d1); - M_Check(&door->d2); - M_Check(&door->d1flip); - M_Check(&door->d2flip); - Item_Animate(item); -} - -void Door_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll) -{ - ITEM *const item = Item_Get(item_num); - if (!Lara_TestBoundsCollide(item, coll->radius)) { - return; - } - if (!Collide_TestCollision(item, lara_item)) { - return; - } - if (coll->enable_baddie_push) { - if (item->current_anim_state != item->goal_anim_state) { - Lara_Push(item, coll, coll->enable_hit, true); - } else { - Lara_Push(item, coll, false, true); - } - } -} - -REGISTER_OBJECT(O_DOOR_TYPE_1, M_Setup) -REGISTER_OBJECT(O_DOOR_TYPE_2, M_Setup) -REGISTER_OBJECT(O_DOOR_TYPE_3, M_Setup) -REGISTER_OBJECT(O_DOOR_TYPE_4, M_Setup) -REGISTER_OBJECT(O_DOOR_TYPE_5, M_Setup) -REGISTER_OBJECT(O_DOOR_TYPE_6, M_Setup) -REGISTER_OBJECT(O_DOOR_TYPE_7, M_Setup) -REGISTER_OBJECT(O_DOOR_TYPE_8, M_Setup) diff --git a/src/tr1/global/types.h b/src/tr1/global/types.h index 54dd99625..6b7384d57 100644 --- a/src/tr1/global/types.h +++ b/src/tr1/global/types.h @@ -123,12 +123,6 @@ typedef struct { }; } PHD_VBUF; -typedef struct { - SECTOR *sector; - SECTOR old_sector; - int16_t block; -} DOORPOS_DATA; - typedef struct { PASSPORT_MODE passport_selection; int32_t select_save_slot; diff --git a/src/tr1/meson.build b/src/tr1/meson.build index 277258aec..71f6c3254 100644 --- a/src/tr1/meson.build +++ b/src/tr1/meson.build @@ -197,7 +197,6 @@ sources = [ 'game/objects/general/cabin.c', 'game/objects/general/camera_target.c', 'game/objects/general/cog.c', - 'game/objects/general/door.c', 'game/objects/general/earthquake.c', 'game/objects/general/keyhole.c', 'game/objects/general/moving_bar.c', diff --git a/src/tr2/game/objects/common.h b/src/tr2/game/objects/common.h index 15a9e8cc0..65911e138 100644 --- a/src/tr2/game/objects/common.h +++ b/src/tr2/game/objects/common.h @@ -6,7 +6,6 @@ void Object_DrawDummyItem(const ITEM *item); void Object_DrawAnimatingItem(const ITEM *item); -void Object_DrawUnclippedItem(const ITEM *item); void Object_DrawSpriteItem(const ITEM *item); void Object_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); diff --git a/src/tr2/global/types_decomp.h b/src/tr2/global/types_decomp.h index 8427402b1..3d1825df8 100644 --- a/src/tr2/global/types_decomp.h +++ b/src/tr2/global/types_decomp.h @@ -120,12 +120,6 @@ typedef enum { GFE_REMOVE_AMMO = 22, } GF_EVENTS; -typedef struct { - SECTOR *sector; - SECTOR old_sector; - int16_t block; -} DOORPOS_DATA; - typedef enum { TRAP_SET = 0, TRAP_ACTIVATE = 1, diff --git a/src/tr2/meson.build b/src/tr2/meson.build index ea203d6bf..80c5e9c77 100644 --- a/src/tr2/meson.build +++ b/src/tr2/meson.build @@ -195,7 +195,6 @@ sources = [ 'game/objects/general/cutscene_player.c', 'game/objects/general/detonator.c', 'game/objects/general/ding_dong.c', - 'game/objects/general/door.c', 'game/objects/general/earthquake.c', 'game/objects/general/final_cutscene.c', 'game/objects/general/final_level_counter.c', From 864589bf0a65b1434ea40f54c1e212fc0f337d6f Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 11:23:46 +0200 Subject: [PATCH 25/52] game-strings: use OG JSON as a fallback in expansions Resolves #2847. --- docs/tr1/CHANGELOG.md | 1 + docs/tr2/CHANGELOG.md | 1 + src/libtrx/game/game_string_table/common.c | 52 ++++++++++++++++--- src/libtrx/game/game_string_table/priv.h | 5 +- src/libtrx/game/game_string_table/reader.c | 32 ++++-------- .../include/libtrx/game/game_string_table.h | 7 +-- src/tr1/game/shell.c | 8 ++- src/tr2/game/shell/common.c | 11 +++- 8 files changed, 81 insertions(+), 36 deletions(-) diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index 4121e32f7..0c6d393bc 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -11,6 +11,7 @@ - changed the "enable EIDOS logo" option to disable the Core Design and Bink Video Codec FMVs as well; renamed to "enable legal" (#2741) - changed sprite pickups to respect the water tint if placed underwater (#2673) - changed save to take priority over load when both inputs are held on the same frame, in line with OG (#2833) +- changed The Unfinished Business strings to default to the OG strings file for the main tables (#2847) - fixed the bilinear filter to not readjust the UVs (#2258) - fixed disabling the cutscenes causing the game to exit (#2743, regression from 4.8) - fixed anisotropy filter causing black lines on certain GPUs (#902) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index ad9328daf..dc2942abc 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,4 +1,5 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...develop) - ××××-××-×× +- changed The Golden Mask strings to default to the OG strings file for the main tables (#2847) - fixed Lara voiding if she stops on a tile with a closing door, and the door isn't on a portal (#2848) - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) - fixed guns carried by enemies not being converted to ammo if Lara starts the level with the gun and the game has later been reloaded (#2850, regression from 1.0) diff --git a/src/libtrx/game/game_string_table/common.c b/src/libtrx/game/game_string_table/common.c index d3f8327dc..9e40ae432 100644 --- a/src/libtrx/game/game_string_table/common.c +++ b/src/libtrx/game/game_string_table/common.c @@ -1,9 +1,11 @@ #include "debug.h" +#include "filesystem.h" #include "game/game_flow.h" #include "game/game_string.h" #include "game/game_string_table.h" #include "game/game_string_table/priv.h" #include "game/objects/names.h" +#include "game/shell.h" #include "log.h" #include "memory.h" @@ -11,8 +13,6 @@ typedef void (*M_LOAD_STRING_FUNC)(const char *, const char *); -GS_FILE g_GST_File = {}; - static struct { GAME_OBJECT_ID target_object_id; GAME_OBJECT_ID source_object_id; @@ -27,6 +27,8 @@ static struct { { .target_object_id = NO_OBJECT }, }; +static VECTOR *m_GST_Layers = nullptr; + static void M_Apply(const GS_TABLE *table); static void M_ApplyLevelTitles( const GS_FILE *gs_file, GF_LEVEL_TABLE_TYPE level_table_type); @@ -90,17 +92,19 @@ static void M_ApplyLevelTitles( GF_GetLevelTable(level_table_type); const GS_LEVEL_TABLE *const gs_level_table = &gs_file->level_tables[level_table_type]; + if (gs_level_table->count == 0) { + return; + } + ASSERT(gs_level_table->count == level_table->count); for (int32_t i = 0; i < level_table->count; i++) { GF_SetLevelTitle( &level_table->levels[i], gs_level_table->entries[i].title); } } -void GameStringTable_Apply(const GF_LEVEL *const level) +static void M_ApplyLayer( + const GF_LEVEL *const level, const GS_FILE *const gs_file) { - const GS_FILE *const gs_file = &g_GST_File; - - Object_ResetNames(); M_Apply(&gs_file->global); for (int32_t i = 0; i < GFLT_NUMBER_OF; i++) { @@ -126,10 +130,44 @@ void GameStringTable_Apply(const GF_LEVEL *const level) M_Apply(&gs_level_table->entries[level->num].table); } } +} + +void GameStringTable_Apply(const GF_LEVEL *const level) +{ + Object_ResetNames(); + ASSERT(m_GST_Layers != nullptr); + for (int32_t i = 0; i < m_GST_Layers->count; i++) { + const GS_FILE *const gs_file = Vector_Get(m_GST_Layers, i); + M_ApplyLayer(level, gs_file); + } M_DoObjectAliases(); } +void GameStringTable_Init(void) +{ + m_GST_Layers = Vector_Create(sizeof(GS_FILE)); +} + void GameStringTable_Shutdown(void) { - GS_File_Free(&g_GST_File); + if (m_GST_Layers != nullptr) { + for (int32_t i = 0; i < m_GST_Layers->count; i++) { + GS_FILE *const gs_file = Vector_Get(m_GST_Layers, i); + GS_File_Free(gs_file); + } + Vector_Free(m_GST_Layers); + m_GST_Layers = nullptr; + } +} + +void GameStringTable_Load(const char *const path, const bool load_levels) +{ + char *data = nullptr; + if (!File_Load(path, &data, nullptr)) { + Shell_ExitSystemFmt("failed to open strings file (path: %d)", path); + } + GS_FILE *gs_file = GS_File_CreateFromString(data, load_levels); + ASSERT(m_GST_Layers != nullptr); + Vector_Add(m_GST_Layers, gs_file); + Memory_FreePointer(&data); } diff --git a/src/libtrx/game/game_string_table/priv.h b/src/libtrx/game/game_string_table/priv.h index 00d69b7f0..9533c5989 100644 --- a/src/libtrx/game/game_string_table/priv.h +++ b/src/libtrx/game/game_string_table/priv.h @@ -1,6 +1,7 @@ #pragma once #include "game/game_flow/enum.h" +#include "vector.h" #include @@ -35,7 +36,7 @@ typedef struct { GS_LEVEL_TABLE level_tables[GFLT_NUMBER_OF]; } GS_FILE; -extern GS_FILE g_GST_File; - void GS_Table_Free(GS_TABLE *gs_table); + +GS_FILE *GS_File_CreateFromString(const char *data, bool load_levels); void GS_File_Free(GS_FILE *gs_file); diff --git a/src/libtrx/game/game_string_table/reader.c b/src/libtrx/game/game_string_table/reader.c index 4cec681d4..5ded75216 100644 --- a/src/libtrx/game/game_string_table/reader.c +++ b/src/libtrx/game/game_string_table/reader.c @@ -1,4 +1,3 @@ -#include "filesystem.h" #include "game/game_flow.h" #include "game/game_string_table.h" #include "game/game_string_table/priv.h" @@ -130,24 +129,14 @@ static void M_LoadLevelsFromJSON( } } -void GameStringTable_LoadFromFile(const char *const path) +GS_FILE *GS_File_CreateFromString( + const char *const data, const bool load_levels) { - char *data = nullptr; - if (!File_Load(path, &data, nullptr)) { - Shell_ExitSystemFmt("failed to open strings file (path: %d)", path); - } - GameStringTable_LoadFromString(data); - Memory_FreePointer(&data); -} - -void GameStringTable_LoadFromString(const char *const data) -{ - GameStringTable_Shutdown(); - - JSON_VALUE *root = nullptr; + GS_FILE *const gs_file = Memory_Alloc(sizeof(GS_FILE)); JSON_PARSE_RESULT parse_result; - root = JSON_ParseEx( + + JSON_VALUE *root = JSON_ParseEx( data, strlen(data), JSON_PARSE_FLAGS_ALLOW_JSON5, nullptr, nullptr, &parse_result); if (root == nullptr) { @@ -157,15 +146,16 @@ void GameStringTable_LoadFromString(const char *const data) parse_result.error_line_no, parse_result.error_row_no, data); } - GS_FILE *const gs_file = &g_GST_File; JSON_OBJECT *root_obj = JSON_ValueAsObject(root); M_LoadTableFromJSON(root_obj, &gs_file->global); - M_LoadLevelsFromJSON(root_obj, gs_file, "levels", GFLT_MAIN); - M_LoadLevelsFromJSON(root_obj, gs_file, "demos", GFLT_DEMOS); - M_LoadLevelsFromJSON(root_obj, gs_file, "cutscenes", GFLT_CUTSCENES); - + if (load_levels) { + M_LoadLevelsFromJSON(root_obj, gs_file, "levels", GFLT_MAIN); + M_LoadLevelsFromJSON(root_obj, gs_file, "demos", GFLT_DEMOS); + M_LoadLevelsFromJSON(root_obj, gs_file, "cutscenes", GFLT_CUTSCENES); + } if (root != nullptr) { JSON_ValueFree(root); root = nullptr; } + return gs_file; } diff --git a/src/libtrx/include/libtrx/game/game_string_table.h b/src/libtrx/include/libtrx/game/game_string_table.h index b14a99c97..cc33f3fe4 100644 --- a/src/libtrx/include/libtrx/game/game_string_table.h +++ b/src/libtrx/include/libtrx/game/game_string_table.h @@ -2,7 +2,8 @@ #include -void GameStringTable_LoadFromFile(const char *path); -void GameStringTable_LoadFromString(const char *data); -void GameStringTable_Apply(const GF_LEVEL *level); +void GameStringTable_Init(void); void GameStringTable_Shutdown(void); + +void GameStringTable_Load(const char *path, bool load_levels); +void GameStringTable_Apply(const GF_LEVEL *level); diff --git a/src/tr1/game/shell.c b/src/tr1/game/shell.c index f892719ba..b8f190715 100644 --- a/src/tr1/game/shell.c +++ b/src/tr1/game/shell.c @@ -152,6 +152,8 @@ void Shell_Shutdown(void) Console_Shutdown(); GameBuf_Shutdown(); Savegame_Shutdown(); + + GameStringTable_Shutdown(); GF_Shutdown(); Output_Shutdown(); @@ -206,7 +208,11 @@ void Shell_Main(void) GF_Init(); GF_LoadFromFile(m_ModPaths[m_Args.mod].game_flow_path); - GameStringTable_LoadFromFile(m_ModPaths[m_Args.mod].game_strings_path); + GameStringTable_Init(); + if (m_Args.mod != M_MOD_OG) { + GameStringTable_Load(m_ModPaths[M_MOD_OG].game_strings_path, false); + } + GameStringTable_Load(m_ModPaths[m_Args.mod].game_strings_path, true); GameStringTable_Apply(nullptr); Savegame_Init(); diff --git a/src/tr2/game/shell/common.c b/src/tr2/game/shell/common.c index f7d00b73d..d0f32b1ab 100644 --- a/src/tr2/game/shell/common.c +++ b/src/tr2/game/shell/common.c @@ -478,7 +478,12 @@ void Shell_Main(void) GF_Init(); GF_LoadFromFile(m_ModPaths[m_Args.mod].game_flow_path); - GameStringTable_LoadFromFile(m_ModPaths[m_Args.mod].game_strings_path); + + GameStringTable_Init(); + if (m_Args.mod != M_MOD_OG) { + GameStringTable_Load(m_ModPaths[M_MOD_OG].game_strings_path, false); + } + GameStringTable_Load(m_ModPaths[m_Args.mod].game_strings_path, true); GameStringTable_Apply(nullptr); GameBuf_Init(); @@ -582,8 +587,9 @@ void Shell_Main(void) void Shell_Shutdown(void) { + GameStringTable_Shutdown(); GF_Shutdown(); - GameString_Shutdown(); + Console_Shutdown(); Render_Shutdown(); Text_Shutdown(); @@ -591,6 +597,7 @@ void Shell_Shutdown(void) GameBuf_Shutdown(); Config_Shutdown(); EnumMap_Shutdown(); + GameString_Shutdown(); } const char *Shell_GetConfigPath(void) From e439371c66cd39575c0e9b146b58245f0bd78fe1 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 16:00:16 +0200 Subject: [PATCH 26/52] audio: fix wrong benchmark times The decoder benchmark was formatting the wrong variable and produced meaningless results in the logs. --- src/libtrx/engine/audio_sample.c | 105 +++++++++++++++++++------------ 1 file changed, 64 insertions(+), 41 deletions(-) diff --git a/src/libtrx/engine/audio_sample.c b/src/libtrx/engine/audio_sample.c index 0d3a4afb8..a4632faa2 100644 --- a/src/libtrx/engine/audio_sample.c +++ b/src/libtrx/engine/audio_sample.c @@ -1,5 +1,6 @@ #include "audio_internal.h" +#include "benchmark.h" #include "debug.h" #include "log.h" #include "memory.h" @@ -21,7 +22,6 @@ #include #include #include -#include typedef struct { char *original_data; @@ -50,8 +50,8 @@ typedef struct { } AUDIO_SAMPLE_SOUND; typedef struct { - const char *data; - const char *ptr; + const uint8_t *data; + const uint8_t *ptr; int32_t size; int32_t remaining; } AUDIO_AV_BUFFER; @@ -64,7 +64,7 @@ static double M_DecibelToMultiplier(double db_gain); static bool M_RecalculateChannelVolumes(int32_t sound_id); static int32_t M_ReadAVBuffer(void *opaque, uint8_t *dst, int32_t dst_size); static int64_t M_SeekAVBuffer(void *opaque, int64_t offset, int32_t whence); -static bool M_Convert(const int32_t sample_id); +static bool M_ConvertSample(const int32_t sample_id); static double M_DecibelToMultiplier(double db_gain) { @@ -135,18 +135,13 @@ static int64_t M_SeekAVBuffer(void *opaque, int64_t offset, int32_t whence) return src->ptr - src->data; } -static bool M_Convert(const int32_t sample_id) +static bool M_ConvertRawData( + const uint8_t *const original_data, const int32_t original_size, + const int32_t dst_sample_rate, const int32_t dst_format, + const int32_t dst_channel_count, uint8_t **const out_sample_data, + size_t *const out_size, size_t *const out_sample_count) { - ASSERT(sample_id >= 0 && sample_id < m_LoadedSamplesCount); - bool result = false; - AUDIO_SAMPLE *const sample = &m_LoadedSamples[sample_id]; - - if (sample->sample_data != nullptr) { - return true; - } - - const clock_t time_start = clock(); size_t working_buffer_size = 0; float *working_buffer = nullptr; @@ -188,10 +183,10 @@ static bool M_Convert(const int32_t sample_id) } AUDIO_AV_BUFFER av_buf = { - .data = sample->original_data, - .ptr = sample->original_data, - .size = sample->original_size, - .remaining = sample->original_size, + .data = original_data, + .ptr = original_data, + .size = original_size, + .remaining = original_size, }; av.avio_context = avio_alloc_context( @@ -248,7 +243,7 @@ static bool M_Convert(const int32_t sample_id) } av.packet = av_packet_alloc(); - if (!av.packet) { + if (av.packet == nullptr) { error_code = AVERROR(ENOMEM); goto cleanup; } @@ -283,7 +278,7 @@ static bool M_Convert(const int32_t sample_id) swr.src.ch_layout = av.codec_ctx->ch_layout; swr.src.format = av.codec_ctx->sample_fmt; swr.dst.sample_rate = AUDIO_WORKING_RATE; - av_channel_layout_default(&swr.dst.ch_layout, 1); + av_channel_layout_default(&swr.dst.ch_layout, dst_channel_count); swr.dst.format = Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT); swr_alloc_set_opts2( &swr.ctx, &swr.dst.ch_layout, swr.dst.format, @@ -351,25 +346,24 @@ static bool M_Convert(const int32_t sample_id) av_packet_unref(av.packet); } - int32_t sample_format_bytes = av_get_bytes_per_sample(swr.dst.format); - sample->num_samples = working_buffer_size / sample_format_bytes - / swr.dst.ch_layout.nb_channels; - sample->channels = swr.src.ch_layout.nb_channels; - sample->sample_data = working_buffer; + if (out_size != nullptr) { + *out_size = working_buffer_size; + } + if (out_sample_count != nullptr) { + *out_sample_count = (int32_t)(working_buffer_size + / av_get_bytes_per_sample(swr.dst.format)) + / swr.dst.ch_layout.nb_channels; + } + if (out_sample_data != nullptr) { + *out_sample_data = (uint8_t *)working_buffer; + } else { + Memory_FreePointer(&working_buffer); + } result = true; - const clock_t time_end = clock(); - const double time_delta = - (((double)(time_end - time_start)) / CLOCKS_PER_SEC) * 1000.0f; - LOG_DEBUG( - "Sample %d decoded (%.0f ms)", sample_id, sample->original_size, - time_delta); - cleanup: if (error_code != 0) { - LOG_ERROR( - "Error while opening sample ID %d: %s", sample_id, - av_err2str(error_code)); + LOG_ERROR("Error while decoding sample: %s", av_err2str(error_code)); } if (swr.ctx) { @@ -387,11 +381,15 @@ cleanup: av.codec = nullptr; if (!result) { - sample->sample_data = nullptr; - sample->original_data = nullptr; - sample->original_size = 0; - sample->num_samples = 0; - sample->channels = 0; + if (out_size != nullptr) { + *out_size = 0; + } + if (out_sample_count != nullptr) { + *out_sample_count = 0; + } + if (out_sample_data != nullptr) { + *out_sample_data = nullptr; + } Memory_FreePointer(&working_buffer); } @@ -411,6 +409,31 @@ cleanup: return result; } +static bool M_ConvertSample(const int32_t sample_id) +{ + ASSERT(sample_id >= 0 && sample_id < m_LoadedSamplesCount); + AUDIO_SAMPLE *const sample = &m_LoadedSamples[sample_id]; + if (sample->sample_data != nullptr) { + return true; + } + + size_t num_samples; + BENCHMARK benchmark = Benchmark_Start(); + + const bool result = M_ConvertRawData( + (uint8_t *)sample->original_data, sample->original_size, + AUDIO_WORKING_RATE, Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT), 1, + (uint8_t **)&sample->sample_data, nullptr, &num_samples); + + char buffer[80]; + sprintf(buffer, "sample %d decoded", sample_id); + Benchmark_End(&benchmark, buffer); + + sample->channels = 1; + sample->num_samples = num_samples; + return result; +} + void Audio_Sample_Init(void) { for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES; @@ -553,7 +576,7 @@ int32_t Audio_Sample_Play( continue; } - M_Convert(sample_id); + M_ConvertSample(sample_id); sound->is_used = true; sound->is_playing = true; From 3696e849259660c938091ff3971af918b497c6b7 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 16:19:47 +0200 Subject: [PATCH 27/52] audio: split into smaller functions --- docs/tr1/CHANGELOG.md | 1 + docs/tr2/CHANGELOG.md | 1 + src/libtrx/engine/audio_sample.c | 267 ++++++++++++++++--------------- 3 files changed, 140 insertions(+), 129 deletions(-) diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index d7a272bab..21aa2d9b1 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -39,6 +39,7 @@ - fixed Story So Far not playing the opening FMV, `cafe.rpl` (#2779, regression from 2.10) - fixed Lara at times ending up in incorrect rooms when using the teleport cheat (#2486, regression from 3.0) - fixed the `/pos` console command reporting the base room number when Lara is actually in a flipped room (#2487, regression from 3.0) +- fixed clicks in audio sounds (#2846, regression from 2.0) - improved bubble appearance (#2672) - improved rendering performance - improved pause exit dialog - it can now be canceled with escape diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index b897619f2..e2f8f8c7b 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -7,6 +7,7 @@ - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) - fixed guns carried by enemies not being converted to ammo if Lara starts the level with the gun and the game has later been reloaded (#2850, regression from 1.0) - fixed 1920x1080 screenshots in 16:9 aspect mode being saved as 1919x1080 (#2845, regression from 0.8) +- fixed clicks in audio sounds (#2846, regression from 0.2) ## [1.0.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...tr2-1.0.1) - 2025-04-24 - added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) diff --git a/src/libtrx/engine/audio_sample.c b/src/libtrx/engine/audio_sample.c index a4632faa2..74de9fcc8 100644 --- a/src/libtrx/engine/audio_sample.c +++ b/src/libtrx/engine/audio_sample.c @@ -23,10 +23,20 @@ #include #include +typedef struct { + struct { + int32_t format; + AVChannelLayout ch_layout; + int32_t sample_rate; + } src, dst; + SwrContext *ctx; + size_t working_buffer_size; + uint8_t *working_buffer; +} M_SWR_CONTEXT; + typedef struct { char *original_data; size_t original_size; - float *sample_data; int32_t channels; int32_t num_samples; @@ -135,6 +145,76 @@ static int64_t M_SeekAVBuffer(void *opaque, int64_t offset, int32_t whence) return src->ptr - src->data; } +static int32_t M_OutputAudioFrame( + M_SWR_CONTEXT *const swr, AVFrame *const frame) +{ + uint8_t *out_buffer = nullptr; + const int32_t out_samples = + swr_get_out_samples(swr->ctx, frame->nb_samples); + av_samples_alloc( + &out_buffer, nullptr, swr->dst.ch_layout.nb_channels, out_samples, + swr->dst.format, 1); + int32_t resampled_size = swr_convert( + swr->ctx, &out_buffer, out_samples, (const uint8_t **)frame->data, + frame->nb_samples); + while (resampled_size > 0) { + int32_t out_buffer_size = av_samples_get_buffer_size( + nullptr, swr->dst.ch_layout.nb_channels, resampled_size, + swr->dst.format, 1); + + if (out_buffer_size > 0) { + swr->working_buffer = Memory_Realloc( + swr->working_buffer, + swr->working_buffer_size + out_buffer_size); + if (out_buffer) { + memcpy( + swr->working_buffer + swr->working_buffer_size, out_buffer, + out_buffer_size); + } + swr->working_buffer_size += out_buffer_size; + } + + resampled_size = + swr_convert(swr->ctx, &out_buffer, out_samples, nullptr, 0); + } + + av_freep(&out_buffer); + return 0; +} + +static int32_t M_DecodePacket( + AVCodecContext *const dec, const AVPacket *const pkt, AVFrame *frame, + M_SWR_CONTEXT *const swr) +{ + // Submit the packet to the decoder + int32_t ret = avcodec_send_packet(dec, pkt); + if (ret < 0) { + LOG_ERROR( + "Error submitting a packet for decoding (%s)\n", av_err2str(ret)); + return ret; + } + + // Get all the available frames from the decoder + while (ret >= 0) { + ret = avcodec_receive_frame(dec, frame); + if (ret < 0) { + // those two return values are special and mean there is no output + // frame available, but there were no errors during decoding + if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN)) { + return 0; + } + LOG_ERROR( + "Error receiving a frame for decoding (%s)\n", av_err2str(ret)); + return ret; + } + + ret = M_OutputAudioFrame(swr, frame); + av_frame_unref(frame); + } + + return ret; +} + static bool M_ConvertRawData( const uint8_t *const original_data, const int32_t original_size, const int32_t dst_sample_rate, const int32_t dst_format, @@ -142,8 +222,6 @@ static bool M_ConvertRawData( size_t *const out_size, size_t *const out_sample_count) { bool result = false; - size_t working_buffer_size = 0; - float *working_buffer = nullptr; struct { size_t read_buffer_size; @@ -165,19 +243,11 @@ static bool M_ConvertRawData( .frame = nullptr, }; - struct { - struct { - int32_t format; - AVChannelLayout ch_layout; - int32_t sample_rate; - } src, dst; - SwrContext *ctx; - } swr = {}; - + M_SWR_CONTEXT swr = {}; int32_t error_code; - unsigned char *read_buffer = av_malloc(av.read_buffer_size); - if (!read_buffer) { + uint8_t *const read_buffer = av_malloc(av.read_buffer_size); + if (read_buffer == nullptr) { error_code = AVERROR(ENOMEM); goto cleanup; } @@ -195,8 +265,7 @@ static bool M_ConvertRawData( av.format_ctx = avformat_alloc_context(); av.format_ctx->pb = av.avio_context; - error_code = - avformat_open_input(&av.format_ctx, "dummy_filename", nullptr, nullptr); + error_code = avformat_open_input(&av.format_ctx, "mem:", nullptr, nullptr); if (error_code != 0) { goto cleanup; } @@ -214,19 +283,19 @@ static bool M_ConvertRawData( break; } } - if (!av.stream) { + if (av.stream == nullptr) { error_code = AVERROR_STREAM_NOT_FOUND; goto cleanup; } av.codec = avcodec_find_decoder(av.stream->codecpar->codec_id); - if (!av.codec) { + if (av.codec == nullptr) { error_code = AVERROR_DEMUXER_NOT_FOUND; goto cleanup; } av.codec_ctx = avcodec_alloc_context3(av.codec); - if (!av.codec_ctx) { + if (av.codec_ctx == nullptr) { error_code = AVERROR(ENOMEM); goto cleanup; } @@ -249,115 +318,62 @@ static bool M_ConvertRawData( } av.frame = av_frame_alloc(); - if (!av.frame) { + if (av.frame == nullptr) { error_code = AVERROR(ENOMEM); goto cleanup; } - while (1) { - error_code = av_read_frame(av.format_ctx, av.packet); - if (error_code == AVERROR_EOF) { - av_packet_unref(av.packet); - error_code = 0; + swr.src.sample_rate = av.codec_ctx->sample_rate; + swr.src.ch_layout = av.codec_ctx->ch_layout; + swr.src.format = av.codec_ctx->sample_fmt; + swr.dst.sample_rate = AUDIO_WORKING_RATE; + av_channel_layout_default(&swr.dst.ch_layout, dst_channel_count); + swr.dst.format = Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT); + swr_alloc_set_opts2( + &swr.ctx, &swr.dst.ch_layout, swr.dst.format, swr.dst.sample_rate, + &swr.src.ch_layout, swr.src.format, swr.src.sample_rate, 0, 0); + if (swr.ctx == nullptr) { + av_packet_unref(av.packet); + error_code = AVERROR(ENOMEM); + goto cleanup; + } + + error_code = swr_init(swr.ctx); + if (error_code != 0) { + av_packet_unref(av.packet); + goto cleanup; + } + + while ((error_code = av_read_frame(av.format_ctx, av.packet)) >= 0) { + M_DecodePacket(av.codec_ctx, av.packet, av.frame, &swr); + av_packet_unref(av.packet); + if (error_code < 0) { break; } + } - if (error_code < 0) { - av_packet_unref(av.packet); - goto cleanup; - } + if (av.codec_ctx != nullptr) { + M_DecodePacket(av.codec_ctx, nullptr, av.frame, &swr); + } - error_code = avcodec_send_packet(av.codec_ctx, av.packet); - if (error_code < 0) { - av_packet_unref(av.packet); - goto cleanup; - } - - if (swr.ctx == nullptr) { - swr.src.sample_rate = av.codec_ctx->sample_rate; - swr.src.ch_layout = av.codec_ctx->ch_layout; - swr.src.format = av.codec_ctx->sample_fmt; - swr.dst.sample_rate = AUDIO_WORKING_RATE; - av_channel_layout_default(&swr.dst.ch_layout, dst_channel_count); - swr.dst.format = Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT); - swr_alloc_set_opts2( - &swr.ctx, &swr.dst.ch_layout, swr.dst.format, - swr.dst.sample_rate, &swr.src.ch_layout, swr.src.format, - swr.src.sample_rate, 0, 0); - if (swr.ctx == nullptr) { - av_packet_unref(av.packet); - error_code = AVERROR(ENOMEM); - goto cleanup; - } - - error_code = swr_init(swr.ctx); - if (error_code != 0) { - av_packet_unref(av.packet); - goto cleanup; - } - } - - while (1) { - error_code = avcodec_receive_frame(av.codec_ctx, av.frame); - if (error_code == AVERROR(EAGAIN)) { - av_frame_unref(av.frame); - break; - } - - if (error_code < 0) { - av_packet_unref(av.packet); - av_frame_unref(av.frame); - goto cleanup; - } - - uint8_t *out_buffer = nullptr; - const int32_t out_samples = - swr_get_out_samples(swr.ctx, av.frame->nb_samples); - av_samples_alloc( - &out_buffer, nullptr, swr.dst.ch_layout.nb_channels, - out_samples, swr.dst.format, 1); - int32_t resampled_size = swr_convert( - swr.ctx, &out_buffer, out_samples, - (const uint8_t **)av.frame->data, av.frame->nb_samples); - while (resampled_size > 0) { - int32_t out_buffer_size = av_samples_get_buffer_size( - nullptr, swr.dst.ch_layout.nb_channels, resampled_size, - swr.dst.format, 1); - - if (out_buffer_size > 0) { - working_buffer = Memory_Realloc( - working_buffer, working_buffer_size + out_buffer_size); - if (out_buffer) { - memcpy( - (uint8_t *)working_buffer + working_buffer_size, - out_buffer, out_buffer_size); - } - working_buffer_size += out_buffer_size; - } - - resampled_size = - swr_convert(swr.ctx, &out_buffer, out_samples, nullptr, 0); - } - - av_freep(&out_buffer); - av_frame_unref(av.frame); - } - - av_packet_unref(av.packet); + if (error_code == AVERROR_EOF) { + error_code = 0; + } else if (error_code < 0) { + goto cleanup; } if (out_size != nullptr) { - *out_size = working_buffer_size; + *out_size = swr.working_buffer_size; } if (out_sample_count != nullptr) { - *out_sample_count = (int32_t)(working_buffer_size - / av_get_bytes_per_sample(swr.dst.format)) + *out_sample_count = (int32_t)swr.working_buffer_size + / av_get_bytes_per_sample(swr.dst.format) / swr.dst.ch_layout.nb_channels; } if (out_sample_data != nullptr) { - *out_sample_data = (uint8_t *)working_buffer; + *out_sample_data = swr.working_buffer; } else { - Memory_FreePointer(&working_buffer); + Memory_FreePointer(&swr.working_buffer); } result = true; @@ -366,20 +382,6 @@ cleanup: LOG_ERROR("Error while decoding sample: %s", av_err2str(error_code)); } - if (swr.ctx) { - swr_free(&swr.ctx); - } - - if (av.frame) { - av_frame_free(&av.frame); - } - - if (av.packet) { - av_packet_free(&av.packet); - } - - av.codec = nullptr; - if (!result) { if (out_size != nullptr) { *out_size = 0; @@ -390,22 +392,29 @@ cleanup: if (out_sample_data != nullptr) { *out_sample_data = nullptr; } - Memory_FreePointer(&working_buffer); + Memory_FreePointer(&swr.working_buffer); } + if (swr.ctx) { + swr_free(&swr.ctx); + } + if (av.frame) { + av_frame_free(&av.frame); + } + if (av.packet) { + av_packet_free(&av.packet); + } + av.codec = nullptr; if (av.codec_ctx) { avcodec_free_context(&av.codec_ctx); } - if (av.format_ctx) { avformat_close_input(&av.format_ctx); } - if (av.avio_context) { av_freep(&av.avio_context->buffer); avio_context_free(&av.avio_context); } - return result; } From 0ba717edd593df13918307aa2b37f8cad1245ccf Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 16:38:55 +0200 Subject: [PATCH 28/52] audio: fix clicks in sample decoding Resolves #2846. --- src/libtrx/engine/audio_sample.c | 51 +++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/src/libtrx/engine/audio_sample.c b/src/libtrx/engine/audio_sample.c index 74de9fcc8..5dee0147b 100644 --- a/src/libtrx/engine/audio_sample.c +++ b/src/libtrx/engine/audio_sample.c @@ -148,34 +148,49 @@ static int64_t M_SeekAVBuffer(void *opaque, int64_t offset, int32_t whence) static int32_t M_OutputAudioFrame( M_SWR_CONTEXT *const swr, AVFrame *const frame) { + // Determine the maximum number of output samples this call can produce, + // based on the current delay already inside the resampler plus the new + // input. Using av_rescale_rnd() keeps everything in integer domain and + // avoids cumulative rounding errors. + const int64_t delay = swr_get_delay(swr->ctx, swr->src.sample_rate); + const int32_t out_samples = (int32_t)av_rescale_rnd( + delay + frame->nb_samples, swr->dst.sample_rate, swr->src.sample_rate, + AV_ROUND_UP); + if (out_samples <= 0) { + return 0; // nothing to do + } + uint8_t *out_buffer = nullptr; - const int32_t out_samples = - swr_get_out_samples(swr->ctx, frame->nb_samples); - av_samples_alloc( - &out_buffer, nullptr, swr->dst.ch_layout.nb_channels, out_samples, - swr->dst.format, 1); - int32_t resampled_size = swr_convert( + if (av_samples_alloc( + &out_buffer, nullptr, swr->dst.ch_layout.nb_channels, out_samples, + swr->dst.format, 1) + < 0) { + return AVERROR(ENOMEM); + } + + // Convert – we do *not* drain the resampler here. + const int32_t converted = swr_convert( swr->ctx, &out_buffer, out_samples, (const uint8_t **)frame->data, frame->nb_samples); - while (resampled_size > 0) { - int32_t out_buffer_size = av_samples_get_buffer_size( - nullptr, swr->dst.ch_layout.nb_channels, resampled_size, - swr->dst.format, 1); + if (converted < 0) { + av_freep(&out_buffer); + return converted; // propagate error + } + + if (converted > 0) { + const int32_t out_buffer_size = av_samples_get_buffer_size( + nullptr, swr->dst.ch_layout.nb_channels, converted, swr->dst.format, + 1); if (out_buffer_size > 0) { swr->working_buffer = Memory_Realloc( swr->working_buffer, swr->working_buffer_size + out_buffer_size); - if (out_buffer) { - memcpy( - swr->working_buffer + swr->working_buffer_size, out_buffer, - out_buffer_size); - } + memcpy( + swr->working_buffer + swr->working_buffer_size, out_buffer, + out_buffer_size); swr->working_buffer_size += out_buffer_size; } - - resampled_size = - swr_convert(swr->ctx, &out_buffer, out_samples, nullptr, 0); } av_freep(&out_buffer); From 24b81007ba5b4336acf72a485340a2d949267297 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 16:00:16 +0200 Subject: [PATCH 29/52] audio: fix wrong benchmark times The decoder benchmark was formatting the wrong variable and produced meaningless results in the logs. --- src/libtrx/engine/audio_sample.c | 105 +++++++++++++++++++------------ 1 file changed, 64 insertions(+), 41 deletions(-) diff --git a/src/libtrx/engine/audio_sample.c b/src/libtrx/engine/audio_sample.c index 0d3a4afb8..a4632faa2 100644 --- a/src/libtrx/engine/audio_sample.c +++ b/src/libtrx/engine/audio_sample.c @@ -1,5 +1,6 @@ #include "audio_internal.h" +#include "benchmark.h" #include "debug.h" #include "log.h" #include "memory.h" @@ -21,7 +22,6 @@ #include #include #include -#include typedef struct { char *original_data; @@ -50,8 +50,8 @@ typedef struct { } AUDIO_SAMPLE_SOUND; typedef struct { - const char *data; - const char *ptr; + const uint8_t *data; + const uint8_t *ptr; int32_t size; int32_t remaining; } AUDIO_AV_BUFFER; @@ -64,7 +64,7 @@ static double M_DecibelToMultiplier(double db_gain); static bool M_RecalculateChannelVolumes(int32_t sound_id); static int32_t M_ReadAVBuffer(void *opaque, uint8_t *dst, int32_t dst_size); static int64_t M_SeekAVBuffer(void *opaque, int64_t offset, int32_t whence); -static bool M_Convert(const int32_t sample_id); +static bool M_ConvertSample(const int32_t sample_id); static double M_DecibelToMultiplier(double db_gain) { @@ -135,18 +135,13 @@ static int64_t M_SeekAVBuffer(void *opaque, int64_t offset, int32_t whence) return src->ptr - src->data; } -static bool M_Convert(const int32_t sample_id) +static bool M_ConvertRawData( + const uint8_t *const original_data, const int32_t original_size, + const int32_t dst_sample_rate, const int32_t dst_format, + const int32_t dst_channel_count, uint8_t **const out_sample_data, + size_t *const out_size, size_t *const out_sample_count) { - ASSERT(sample_id >= 0 && sample_id < m_LoadedSamplesCount); - bool result = false; - AUDIO_SAMPLE *const sample = &m_LoadedSamples[sample_id]; - - if (sample->sample_data != nullptr) { - return true; - } - - const clock_t time_start = clock(); size_t working_buffer_size = 0; float *working_buffer = nullptr; @@ -188,10 +183,10 @@ static bool M_Convert(const int32_t sample_id) } AUDIO_AV_BUFFER av_buf = { - .data = sample->original_data, - .ptr = sample->original_data, - .size = sample->original_size, - .remaining = sample->original_size, + .data = original_data, + .ptr = original_data, + .size = original_size, + .remaining = original_size, }; av.avio_context = avio_alloc_context( @@ -248,7 +243,7 @@ static bool M_Convert(const int32_t sample_id) } av.packet = av_packet_alloc(); - if (!av.packet) { + if (av.packet == nullptr) { error_code = AVERROR(ENOMEM); goto cleanup; } @@ -283,7 +278,7 @@ static bool M_Convert(const int32_t sample_id) swr.src.ch_layout = av.codec_ctx->ch_layout; swr.src.format = av.codec_ctx->sample_fmt; swr.dst.sample_rate = AUDIO_WORKING_RATE; - av_channel_layout_default(&swr.dst.ch_layout, 1); + av_channel_layout_default(&swr.dst.ch_layout, dst_channel_count); swr.dst.format = Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT); swr_alloc_set_opts2( &swr.ctx, &swr.dst.ch_layout, swr.dst.format, @@ -351,25 +346,24 @@ static bool M_Convert(const int32_t sample_id) av_packet_unref(av.packet); } - int32_t sample_format_bytes = av_get_bytes_per_sample(swr.dst.format); - sample->num_samples = working_buffer_size / sample_format_bytes - / swr.dst.ch_layout.nb_channels; - sample->channels = swr.src.ch_layout.nb_channels; - sample->sample_data = working_buffer; + if (out_size != nullptr) { + *out_size = working_buffer_size; + } + if (out_sample_count != nullptr) { + *out_sample_count = (int32_t)(working_buffer_size + / av_get_bytes_per_sample(swr.dst.format)) + / swr.dst.ch_layout.nb_channels; + } + if (out_sample_data != nullptr) { + *out_sample_data = (uint8_t *)working_buffer; + } else { + Memory_FreePointer(&working_buffer); + } result = true; - const clock_t time_end = clock(); - const double time_delta = - (((double)(time_end - time_start)) / CLOCKS_PER_SEC) * 1000.0f; - LOG_DEBUG( - "Sample %d decoded (%.0f ms)", sample_id, sample->original_size, - time_delta); - cleanup: if (error_code != 0) { - LOG_ERROR( - "Error while opening sample ID %d: %s", sample_id, - av_err2str(error_code)); + LOG_ERROR("Error while decoding sample: %s", av_err2str(error_code)); } if (swr.ctx) { @@ -387,11 +381,15 @@ cleanup: av.codec = nullptr; if (!result) { - sample->sample_data = nullptr; - sample->original_data = nullptr; - sample->original_size = 0; - sample->num_samples = 0; - sample->channels = 0; + if (out_size != nullptr) { + *out_size = 0; + } + if (out_sample_count != nullptr) { + *out_sample_count = 0; + } + if (out_sample_data != nullptr) { + *out_sample_data = nullptr; + } Memory_FreePointer(&working_buffer); } @@ -411,6 +409,31 @@ cleanup: return result; } +static bool M_ConvertSample(const int32_t sample_id) +{ + ASSERT(sample_id >= 0 && sample_id < m_LoadedSamplesCount); + AUDIO_SAMPLE *const sample = &m_LoadedSamples[sample_id]; + if (sample->sample_data != nullptr) { + return true; + } + + size_t num_samples; + BENCHMARK benchmark = Benchmark_Start(); + + const bool result = M_ConvertRawData( + (uint8_t *)sample->original_data, sample->original_size, + AUDIO_WORKING_RATE, Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT), 1, + (uint8_t **)&sample->sample_data, nullptr, &num_samples); + + char buffer[80]; + sprintf(buffer, "sample %d decoded", sample_id); + Benchmark_End(&benchmark, buffer); + + sample->channels = 1; + sample->num_samples = num_samples; + return result; +} + void Audio_Sample_Init(void) { for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES; @@ -553,7 +576,7 @@ int32_t Audio_Sample_Play( continue; } - M_Convert(sample_id); + M_ConvertSample(sample_id); sound->is_used = true; sound->is_playing = true; From 820fc307d20327a923226b7dfdb6aedc3de9f01c Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 16:19:47 +0200 Subject: [PATCH 30/52] audio: split into smaller functions --- docs/tr1/CHANGELOG.md | 1 + docs/tr2/CHANGELOG.md | 1 + src/libtrx/engine/audio_sample.c | 267 ++++++++++++++++--------------- 3 files changed, 140 insertions(+), 129 deletions(-) diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index 0c6d393bc..e0a5f7868 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -36,6 +36,7 @@ - fixed Story So Far not playing the opening FMV, `cafe.rpl` (#2779, regression from 2.10) - fixed Lara at times ending up in incorrect rooms when using the teleport cheat (#2486, regression from 3.0) - fixed the `/pos` console command reporting the base room number when Lara is actually in a flipped room (#2487, regression from 3.0) +- fixed clicks in audio sounds (#2846, regression from 2.0) - improved bubble appearance (#2672) - improved rendering performance - improved pause exit dialog - it can now be canceled with escape diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index dc2942abc..42602984e 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -4,6 +4,7 @@ - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) - fixed guns carried by enemies not being converted to ammo if Lara starts the level with the gun and the game has later been reloaded (#2850, regression from 1.0) - fixed 1920x1080 screenshots in 16:9 aspect mode being saved as 1919x1080 (#2845, regression from 0.8) +- fixed clicks in audio sounds (#2846, regression from 0.2) ## [1.0.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...tr2-1.0.1) - 2025-04-24 - added an option to wraparound when scrolling UI dialogs, such as save/load (#2834) diff --git a/src/libtrx/engine/audio_sample.c b/src/libtrx/engine/audio_sample.c index a4632faa2..74de9fcc8 100644 --- a/src/libtrx/engine/audio_sample.c +++ b/src/libtrx/engine/audio_sample.c @@ -23,10 +23,20 @@ #include #include +typedef struct { + struct { + int32_t format; + AVChannelLayout ch_layout; + int32_t sample_rate; + } src, dst; + SwrContext *ctx; + size_t working_buffer_size; + uint8_t *working_buffer; +} M_SWR_CONTEXT; + typedef struct { char *original_data; size_t original_size; - float *sample_data; int32_t channels; int32_t num_samples; @@ -135,6 +145,76 @@ static int64_t M_SeekAVBuffer(void *opaque, int64_t offset, int32_t whence) return src->ptr - src->data; } +static int32_t M_OutputAudioFrame( + M_SWR_CONTEXT *const swr, AVFrame *const frame) +{ + uint8_t *out_buffer = nullptr; + const int32_t out_samples = + swr_get_out_samples(swr->ctx, frame->nb_samples); + av_samples_alloc( + &out_buffer, nullptr, swr->dst.ch_layout.nb_channels, out_samples, + swr->dst.format, 1); + int32_t resampled_size = swr_convert( + swr->ctx, &out_buffer, out_samples, (const uint8_t **)frame->data, + frame->nb_samples); + while (resampled_size > 0) { + int32_t out_buffer_size = av_samples_get_buffer_size( + nullptr, swr->dst.ch_layout.nb_channels, resampled_size, + swr->dst.format, 1); + + if (out_buffer_size > 0) { + swr->working_buffer = Memory_Realloc( + swr->working_buffer, + swr->working_buffer_size + out_buffer_size); + if (out_buffer) { + memcpy( + swr->working_buffer + swr->working_buffer_size, out_buffer, + out_buffer_size); + } + swr->working_buffer_size += out_buffer_size; + } + + resampled_size = + swr_convert(swr->ctx, &out_buffer, out_samples, nullptr, 0); + } + + av_freep(&out_buffer); + return 0; +} + +static int32_t M_DecodePacket( + AVCodecContext *const dec, const AVPacket *const pkt, AVFrame *frame, + M_SWR_CONTEXT *const swr) +{ + // Submit the packet to the decoder + int32_t ret = avcodec_send_packet(dec, pkt); + if (ret < 0) { + LOG_ERROR( + "Error submitting a packet for decoding (%s)\n", av_err2str(ret)); + return ret; + } + + // Get all the available frames from the decoder + while (ret >= 0) { + ret = avcodec_receive_frame(dec, frame); + if (ret < 0) { + // those two return values are special and mean there is no output + // frame available, but there were no errors during decoding + if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN)) { + return 0; + } + LOG_ERROR( + "Error receiving a frame for decoding (%s)\n", av_err2str(ret)); + return ret; + } + + ret = M_OutputAudioFrame(swr, frame); + av_frame_unref(frame); + } + + return ret; +} + static bool M_ConvertRawData( const uint8_t *const original_data, const int32_t original_size, const int32_t dst_sample_rate, const int32_t dst_format, @@ -142,8 +222,6 @@ static bool M_ConvertRawData( size_t *const out_size, size_t *const out_sample_count) { bool result = false; - size_t working_buffer_size = 0; - float *working_buffer = nullptr; struct { size_t read_buffer_size; @@ -165,19 +243,11 @@ static bool M_ConvertRawData( .frame = nullptr, }; - struct { - struct { - int32_t format; - AVChannelLayout ch_layout; - int32_t sample_rate; - } src, dst; - SwrContext *ctx; - } swr = {}; - + M_SWR_CONTEXT swr = {}; int32_t error_code; - unsigned char *read_buffer = av_malloc(av.read_buffer_size); - if (!read_buffer) { + uint8_t *const read_buffer = av_malloc(av.read_buffer_size); + if (read_buffer == nullptr) { error_code = AVERROR(ENOMEM); goto cleanup; } @@ -195,8 +265,7 @@ static bool M_ConvertRawData( av.format_ctx = avformat_alloc_context(); av.format_ctx->pb = av.avio_context; - error_code = - avformat_open_input(&av.format_ctx, "dummy_filename", nullptr, nullptr); + error_code = avformat_open_input(&av.format_ctx, "mem:", nullptr, nullptr); if (error_code != 0) { goto cleanup; } @@ -214,19 +283,19 @@ static bool M_ConvertRawData( break; } } - if (!av.stream) { + if (av.stream == nullptr) { error_code = AVERROR_STREAM_NOT_FOUND; goto cleanup; } av.codec = avcodec_find_decoder(av.stream->codecpar->codec_id); - if (!av.codec) { + if (av.codec == nullptr) { error_code = AVERROR_DEMUXER_NOT_FOUND; goto cleanup; } av.codec_ctx = avcodec_alloc_context3(av.codec); - if (!av.codec_ctx) { + if (av.codec_ctx == nullptr) { error_code = AVERROR(ENOMEM); goto cleanup; } @@ -249,115 +318,62 @@ static bool M_ConvertRawData( } av.frame = av_frame_alloc(); - if (!av.frame) { + if (av.frame == nullptr) { error_code = AVERROR(ENOMEM); goto cleanup; } - while (1) { - error_code = av_read_frame(av.format_ctx, av.packet); - if (error_code == AVERROR_EOF) { - av_packet_unref(av.packet); - error_code = 0; + swr.src.sample_rate = av.codec_ctx->sample_rate; + swr.src.ch_layout = av.codec_ctx->ch_layout; + swr.src.format = av.codec_ctx->sample_fmt; + swr.dst.sample_rate = AUDIO_WORKING_RATE; + av_channel_layout_default(&swr.dst.ch_layout, dst_channel_count); + swr.dst.format = Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT); + swr_alloc_set_opts2( + &swr.ctx, &swr.dst.ch_layout, swr.dst.format, swr.dst.sample_rate, + &swr.src.ch_layout, swr.src.format, swr.src.sample_rate, 0, 0); + if (swr.ctx == nullptr) { + av_packet_unref(av.packet); + error_code = AVERROR(ENOMEM); + goto cleanup; + } + + error_code = swr_init(swr.ctx); + if (error_code != 0) { + av_packet_unref(av.packet); + goto cleanup; + } + + while ((error_code = av_read_frame(av.format_ctx, av.packet)) >= 0) { + M_DecodePacket(av.codec_ctx, av.packet, av.frame, &swr); + av_packet_unref(av.packet); + if (error_code < 0) { break; } + } - if (error_code < 0) { - av_packet_unref(av.packet); - goto cleanup; - } + if (av.codec_ctx != nullptr) { + M_DecodePacket(av.codec_ctx, nullptr, av.frame, &swr); + } - error_code = avcodec_send_packet(av.codec_ctx, av.packet); - if (error_code < 0) { - av_packet_unref(av.packet); - goto cleanup; - } - - if (swr.ctx == nullptr) { - swr.src.sample_rate = av.codec_ctx->sample_rate; - swr.src.ch_layout = av.codec_ctx->ch_layout; - swr.src.format = av.codec_ctx->sample_fmt; - swr.dst.sample_rate = AUDIO_WORKING_RATE; - av_channel_layout_default(&swr.dst.ch_layout, dst_channel_count); - swr.dst.format = Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT); - swr_alloc_set_opts2( - &swr.ctx, &swr.dst.ch_layout, swr.dst.format, - swr.dst.sample_rate, &swr.src.ch_layout, swr.src.format, - swr.src.sample_rate, 0, 0); - if (swr.ctx == nullptr) { - av_packet_unref(av.packet); - error_code = AVERROR(ENOMEM); - goto cleanup; - } - - error_code = swr_init(swr.ctx); - if (error_code != 0) { - av_packet_unref(av.packet); - goto cleanup; - } - } - - while (1) { - error_code = avcodec_receive_frame(av.codec_ctx, av.frame); - if (error_code == AVERROR(EAGAIN)) { - av_frame_unref(av.frame); - break; - } - - if (error_code < 0) { - av_packet_unref(av.packet); - av_frame_unref(av.frame); - goto cleanup; - } - - uint8_t *out_buffer = nullptr; - const int32_t out_samples = - swr_get_out_samples(swr.ctx, av.frame->nb_samples); - av_samples_alloc( - &out_buffer, nullptr, swr.dst.ch_layout.nb_channels, - out_samples, swr.dst.format, 1); - int32_t resampled_size = swr_convert( - swr.ctx, &out_buffer, out_samples, - (const uint8_t **)av.frame->data, av.frame->nb_samples); - while (resampled_size > 0) { - int32_t out_buffer_size = av_samples_get_buffer_size( - nullptr, swr.dst.ch_layout.nb_channels, resampled_size, - swr.dst.format, 1); - - if (out_buffer_size > 0) { - working_buffer = Memory_Realloc( - working_buffer, working_buffer_size + out_buffer_size); - if (out_buffer) { - memcpy( - (uint8_t *)working_buffer + working_buffer_size, - out_buffer, out_buffer_size); - } - working_buffer_size += out_buffer_size; - } - - resampled_size = - swr_convert(swr.ctx, &out_buffer, out_samples, nullptr, 0); - } - - av_freep(&out_buffer); - av_frame_unref(av.frame); - } - - av_packet_unref(av.packet); + if (error_code == AVERROR_EOF) { + error_code = 0; + } else if (error_code < 0) { + goto cleanup; } if (out_size != nullptr) { - *out_size = working_buffer_size; + *out_size = swr.working_buffer_size; } if (out_sample_count != nullptr) { - *out_sample_count = (int32_t)(working_buffer_size - / av_get_bytes_per_sample(swr.dst.format)) + *out_sample_count = (int32_t)swr.working_buffer_size + / av_get_bytes_per_sample(swr.dst.format) / swr.dst.ch_layout.nb_channels; } if (out_sample_data != nullptr) { - *out_sample_data = (uint8_t *)working_buffer; + *out_sample_data = swr.working_buffer; } else { - Memory_FreePointer(&working_buffer); + Memory_FreePointer(&swr.working_buffer); } result = true; @@ -366,20 +382,6 @@ cleanup: LOG_ERROR("Error while decoding sample: %s", av_err2str(error_code)); } - if (swr.ctx) { - swr_free(&swr.ctx); - } - - if (av.frame) { - av_frame_free(&av.frame); - } - - if (av.packet) { - av_packet_free(&av.packet); - } - - av.codec = nullptr; - if (!result) { if (out_size != nullptr) { *out_size = 0; @@ -390,22 +392,29 @@ cleanup: if (out_sample_data != nullptr) { *out_sample_data = nullptr; } - Memory_FreePointer(&working_buffer); + Memory_FreePointer(&swr.working_buffer); } + if (swr.ctx) { + swr_free(&swr.ctx); + } + if (av.frame) { + av_frame_free(&av.frame); + } + if (av.packet) { + av_packet_free(&av.packet); + } + av.codec = nullptr; if (av.codec_ctx) { avcodec_free_context(&av.codec_ctx); } - if (av.format_ctx) { avformat_close_input(&av.format_ctx); } - if (av.avio_context) { av_freep(&av.avio_context->buffer); avio_context_free(&av.avio_context); } - return result; } From d97edaf1eb8391bdfc049c0bb6c38eb96322b001 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 16:38:55 +0200 Subject: [PATCH 31/52] audio: fix clicks in sample decoding Resolves #2846. --- src/libtrx/engine/audio_sample.c | 51 +++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/src/libtrx/engine/audio_sample.c b/src/libtrx/engine/audio_sample.c index 74de9fcc8..5dee0147b 100644 --- a/src/libtrx/engine/audio_sample.c +++ b/src/libtrx/engine/audio_sample.c @@ -148,34 +148,49 @@ static int64_t M_SeekAVBuffer(void *opaque, int64_t offset, int32_t whence) static int32_t M_OutputAudioFrame( M_SWR_CONTEXT *const swr, AVFrame *const frame) { + // Determine the maximum number of output samples this call can produce, + // based on the current delay already inside the resampler plus the new + // input. Using av_rescale_rnd() keeps everything in integer domain and + // avoids cumulative rounding errors. + const int64_t delay = swr_get_delay(swr->ctx, swr->src.sample_rate); + const int32_t out_samples = (int32_t)av_rescale_rnd( + delay + frame->nb_samples, swr->dst.sample_rate, swr->src.sample_rate, + AV_ROUND_UP); + if (out_samples <= 0) { + return 0; // nothing to do + } + uint8_t *out_buffer = nullptr; - const int32_t out_samples = - swr_get_out_samples(swr->ctx, frame->nb_samples); - av_samples_alloc( - &out_buffer, nullptr, swr->dst.ch_layout.nb_channels, out_samples, - swr->dst.format, 1); - int32_t resampled_size = swr_convert( + if (av_samples_alloc( + &out_buffer, nullptr, swr->dst.ch_layout.nb_channels, out_samples, + swr->dst.format, 1) + < 0) { + return AVERROR(ENOMEM); + } + + // Convert – we do *not* drain the resampler here. + const int32_t converted = swr_convert( swr->ctx, &out_buffer, out_samples, (const uint8_t **)frame->data, frame->nb_samples); - while (resampled_size > 0) { - int32_t out_buffer_size = av_samples_get_buffer_size( - nullptr, swr->dst.ch_layout.nb_channels, resampled_size, - swr->dst.format, 1); + if (converted < 0) { + av_freep(&out_buffer); + return converted; // propagate error + } + + if (converted > 0) { + const int32_t out_buffer_size = av_samples_get_buffer_size( + nullptr, swr->dst.ch_layout.nb_channels, converted, swr->dst.format, + 1); if (out_buffer_size > 0) { swr->working_buffer = Memory_Realloc( swr->working_buffer, swr->working_buffer_size + out_buffer_size); - if (out_buffer) { - memcpy( - swr->working_buffer + swr->working_buffer_size, out_buffer, - out_buffer_size); - } + memcpy( + swr->working_buffer + swr->working_buffer_size, out_buffer, + out_buffer_size); swr->working_buffer_size += out_buffer_size; } - - resampled_size = - swr_convert(swr->ctx, &out_buffer, out_samples, nullptr, 0); } av_freep(&out_buffer); From 88f41c5a75cc63bc59c160481b26af2a10d134eb Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 17:59:35 +0200 Subject: [PATCH 32/52] tr2/inventory: fix button mashing loading game instead of saving Resolves #2863. --- docs/tr2/CHANGELOG.md | 1 + src/tr1/game/inventory_ring/control.c | 3 +-- src/tr2/game/inventory_ring/control.c | 8 ++++---- src/tr2/game/option/option_passport.c | 24 +++++++++++++++++++----- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index e2f8f8c7b..24f3f05ea 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -5,6 +5,7 @@ - changed the sound dialog appearance (repositioned, added text labels and arrows) - fixed Lara voiding if she stops on a tile with a closing door, and the door isn't on a portal (#2848) - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) +- fixed button mashing triggering load instead of save on a specific passport animation frame (#2863, regression from 1.0) - fixed guns carried by enemies not being converted to ammo if Lara starts the level with the gun and the game has later been reloaded (#2850, regression from 1.0) - fixed 1920x1080 screenshots in 16:9 aspect mode being saved as 1919x1080 (#2845, regression from 0.8) - fixed clicks in audio sounds (#2846, regression from 0.2) diff --git a/src/tr1/game/inventory_ring/control.c b/src/tr1/game/inventory_ring/control.c index 760b66a9b..bb5803ccb 100644 --- a/src/tr1/game/inventory_ring/control.c +++ b/src/tr1/game/inventory_ring/control.c @@ -694,7 +694,6 @@ static GF_COMMAND M_Control(INV_RING *const ring) InvRing_MotionSetup(ring, RNG_CLOSING_ITEM, RNG_DESELECT, 0); g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; - if (ring->mode == INV_LOAD_MODE || ring->mode == INV_SAVE_MODE || ring->mode == INV_SAVE_CRYSTAL_MODE) { InvRing_MotionSetup( @@ -713,7 +712,7 @@ static GF_COMMAND M_Control(INV_RING *const ring) } if (ring->mode == INV_TITLE_MODE - && ((inv_item->object_id == O_DETAIL_OPTION) + && (inv_item->object_id == O_DETAIL_OPTION || inv_item->object_id == O_SOUND_OPTION || inv_item->object_id == O_CONTROL_OPTION || inv_item->object_id == O_GAMMA_OPTION)) { diff --git a/src/tr2/game/inventory_ring/control.c b/src/tr2/game/inventory_ring/control.c index 900198691..5f1b04969 100644 --- a/src/tr2/game/inventory_ring/control.c +++ b/src/tr2/game/inventory_ring/control.c @@ -639,10 +639,10 @@ static GF_COMMAND M_Control(INV_RING *const ring) if (g_InputDB.menu_confirm) { g_Inv_Chosen = inv_item->object_id; - if (ring->type != RT_MAIN) { - g_InvRing_Source[RT_OPTION].current = ring->current_object; - } else { + if (ring->type == RT_MAIN) { g_InvRing_Source[RT_MAIN].current = ring->current_object; + } else { + g_InvRing_Source[RT_OPTION].current = ring->current_object; } if (ring->mode == INV_TITLE_MODE && (inv_item->object_id == O_DETAIL_OPTION @@ -783,7 +783,7 @@ INV_RING *InvRing_Open(const INVENTORY_MODE mode) } for (int32_t i = 0; i < 8; i++) { - g_Inv_ExtraData[i] = 0; + g_Inv_ExtraData[i] = -1; } g_InvRing_Source[RT_MAIN].current = 0; diff --git a/src/tr2/game/option/option_passport.c b/src/tr2/game/option/option_passport.c index 1c56e5f78..1da73aebc 100644 --- a/src/tr2/game/option/option_passport.c +++ b/src/tr2/game/option/option_passport.c @@ -66,6 +66,8 @@ static void M_LoadGame(INVENTORY_ITEM *inv_item); static void M_SaveGame(INVENTORY_ITEM *inv_item); static void M_NewGame(void); static void M_PlayAnyLevel(INVENTORY_ITEM *inv_item); +static int32_t M_GetCurrentPage(const INVENTORY_ITEM *inv_item); +static bool M_IsFlipping(const INVENTORY_ITEM *inv_item); static void M_FlipLeft(INVENTORY_ITEM *inv_item); static void M_FlipRight(INVENTORY_ITEM *inv_item); static void M_Close(INVENTORY_ITEM *inv_item); @@ -318,6 +320,17 @@ static void M_PlayAnyLevel(INVENTORY_ITEM *const inv_item) } } +static int32_t M_GetCurrentPage(const INVENTORY_ITEM *const inv_item) +{ + const int32_t frame = inv_item->goal_frame - inv_item->open_frame; + return frame % 5 == 0 ? frame / 5 : -1; +} + +static bool M_IsFlipping(const INVENTORY_ITEM *const inv_item) +{ + return M_GetCurrentPage(inv_item) == -1; +} + static void M_FlipLeft(INVENTORY_ITEM *const inv_item) { M_RemoveAllText(); @@ -413,18 +426,19 @@ void Option_Passport_Control(INVENTORY_ITEM *const item, const bool is_busy) InvRing_RemoveAllText(); - const int32_t frame = item->goal_frame - item->open_frame; - const int32_t page = frame % 5 == 0 ? frame / 5 : -1; - const bool is_flipping = page == -1; - if (is_flipping) { + if (M_IsFlipping(item)) { return; } - m_State.current_page = page; + m_State.current_page = M_GetCurrentPage(item); if (m_State.current_page < m_State.active_page) { M_FlipRight(item); + g_Input = (INPUT_STATE) {}; + g_InputDB = (INPUT_STATE) {}; } else if (m_State.current_page > m_State.active_page) { M_FlipLeft(item); + g_Input = (INPUT_STATE) {}; + g_InputDB = (INPUT_STATE) {}; } else { m_State.is_ready = true; M_ShowPage(item); From f27d0435a9fbd4a2c90827d554f3de0ecbf6d6ea Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 19:41:37 +0200 Subject: [PATCH 33/52] game-strings: fix crash with -l/--level --- src/libtrx/game/game_string_table/common.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libtrx/game/game_string_table/common.c b/src/libtrx/game/game_string_table/common.c index 9e40ae432..f44260c6c 100644 --- a/src/libtrx/game/game_string_table/common.c +++ b/src/libtrx/game/game_string_table/common.c @@ -124,7 +124,7 @@ static void M_ApplyLayer( } } - if (gs_level_table != nullptr) { + if (gs_level_table != nullptr && gs_level_table->count != 0) { ASSERT(level->num >= 0); ASSERT(level->num < gs_level_table->count); M_Apply(&gs_level_table->entries[level->num].table); From a7269bbe8a6a01a178f31e2bf1f4a4cde78cec1b Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 19:41:51 +0200 Subject: [PATCH 34/52] game-strings: fix memory leak --- src/libtrx/game/game_string_table/common.c | 8 ++++---- src/libtrx/game/game_string_table/priv.c | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libtrx/game/game_string_table/common.c b/src/libtrx/game/game_string_table/common.c index f44260c6c..3d31c1738 100644 --- a/src/libtrx/game/game_string_table/common.c +++ b/src/libtrx/game/game_string_table/common.c @@ -137,7 +137,7 @@ void GameStringTable_Apply(const GF_LEVEL *const level) Object_ResetNames(); ASSERT(m_GST_Layers != nullptr); for (int32_t i = 0; i < m_GST_Layers->count; i++) { - const GS_FILE *const gs_file = Vector_Get(m_GST_Layers, i); + const GS_FILE *const gs_file = *(GS_FILE **)Vector_Get(m_GST_Layers, i); M_ApplyLayer(level, gs_file); } M_DoObjectAliases(); @@ -145,14 +145,14 @@ void GameStringTable_Apply(const GF_LEVEL *const level) void GameStringTable_Init(void) { - m_GST_Layers = Vector_Create(sizeof(GS_FILE)); + m_GST_Layers = Vector_Create(sizeof(GS_FILE *)); } void GameStringTable_Shutdown(void) { if (m_GST_Layers != nullptr) { for (int32_t i = 0; i < m_GST_Layers->count; i++) { - GS_FILE *const gs_file = Vector_Get(m_GST_Layers, i); + GS_FILE *const gs_file = *(GS_FILE **)Vector_Get(m_GST_Layers, i); GS_File_Free(gs_file); } Vector_Free(m_GST_Layers); @@ -168,6 +168,6 @@ void GameStringTable_Load(const char *const path, const bool load_levels) } GS_FILE *gs_file = GS_File_CreateFromString(data, load_levels); ASSERT(m_GST_Layers != nullptr); - Vector_Add(m_GST_Layers, gs_file); + Vector_Add(m_GST_Layers, &gs_file); Memory_FreePointer(&data); } diff --git a/src/libtrx/game/game_string_table/priv.c b/src/libtrx/game/game_string_table/priv.c index 0b62cc41b..a9bada043 100644 --- a/src/libtrx/game/game_string_table/priv.c +++ b/src/libtrx/game/game_string_table/priv.c @@ -52,4 +52,5 @@ void GS_File_Free(GS_FILE *const gs_file) for (int32_t i = 0; i < GFLT_NUMBER_OF; i++) { M_FreeLevelsTable(&gs_file->level_tables[i]); } + Memory_Free(gs_file); } From 0c5b5dbb7b1e7a40f9f5c90e8392c2fa65e88fa4 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 17:59:35 +0200 Subject: [PATCH 35/52] tr2/inventory: fix button mashing loading game instead of saving Resolves #2863. --- docs/tr2/CHANGELOG.md | 1 + src/tr1/game/inventory_ring/control.c | 3 +-- src/tr2/game/inventory_ring/control.c | 8 ++++---- src/tr2/game/option/option_passport.c | 24 +++++++++++++++++++----- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 42602984e..511ab2bb4 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -2,6 +2,7 @@ - changed The Golden Mask strings to default to the OG strings file for the main tables (#2847) - fixed Lara voiding if she stops on a tile with a closing door, and the door isn't on a portal (#2848) - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) +- fixed button mashing triggering load instead of save on a specific passport animation frame (#2863, regression from 1.0) - fixed guns carried by enemies not being converted to ammo if Lara starts the level with the gun and the game has later been reloaded (#2850, regression from 1.0) - fixed 1920x1080 screenshots in 16:9 aspect mode being saved as 1919x1080 (#2845, regression from 0.8) - fixed clicks in audio sounds (#2846, regression from 0.2) diff --git a/src/tr1/game/inventory_ring/control.c b/src/tr1/game/inventory_ring/control.c index 760b66a9b..bb5803ccb 100644 --- a/src/tr1/game/inventory_ring/control.c +++ b/src/tr1/game/inventory_ring/control.c @@ -694,7 +694,6 @@ static GF_COMMAND M_Control(INV_RING *const ring) InvRing_MotionSetup(ring, RNG_CLOSING_ITEM, RNG_DESELECT, 0); g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; - if (ring->mode == INV_LOAD_MODE || ring->mode == INV_SAVE_MODE || ring->mode == INV_SAVE_CRYSTAL_MODE) { InvRing_MotionSetup( @@ -713,7 +712,7 @@ static GF_COMMAND M_Control(INV_RING *const ring) } if (ring->mode == INV_TITLE_MODE - && ((inv_item->object_id == O_DETAIL_OPTION) + && (inv_item->object_id == O_DETAIL_OPTION || inv_item->object_id == O_SOUND_OPTION || inv_item->object_id == O_CONTROL_OPTION || inv_item->object_id == O_GAMMA_OPTION)) { diff --git a/src/tr2/game/inventory_ring/control.c b/src/tr2/game/inventory_ring/control.c index 900198691..5f1b04969 100644 --- a/src/tr2/game/inventory_ring/control.c +++ b/src/tr2/game/inventory_ring/control.c @@ -639,10 +639,10 @@ static GF_COMMAND M_Control(INV_RING *const ring) if (g_InputDB.menu_confirm) { g_Inv_Chosen = inv_item->object_id; - if (ring->type != RT_MAIN) { - g_InvRing_Source[RT_OPTION].current = ring->current_object; - } else { + if (ring->type == RT_MAIN) { g_InvRing_Source[RT_MAIN].current = ring->current_object; + } else { + g_InvRing_Source[RT_OPTION].current = ring->current_object; } if (ring->mode == INV_TITLE_MODE && (inv_item->object_id == O_DETAIL_OPTION @@ -783,7 +783,7 @@ INV_RING *InvRing_Open(const INVENTORY_MODE mode) } for (int32_t i = 0; i < 8; i++) { - g_Inv_ExtraData[i] = 0; + g_Inv_ExtraData[i] = -1; } g_InvRing_Source[RT_MAIN].current = 0; diff --git a/src/tr2/game/option/option_passport.c b/src/tr2/game/option/option_passport.c index 1c56e5f78..1da73aebc 100644 --- a/src/tr2/game/option/option_passport.c +++ b/src/tr2/game/option/option_passport.c @@ -66,6 +66,8 @@ static void M_LoadGame(INVENTORY_ITEM *inv_item); static void M_SaveGame(INVENTORY_ITEM *inv_item); static void M_NewGame(void); static void M_PlayAnyLevel(INVENTORY_ITEM *inv_item); +static int32_t M_GetCurrentPage(const INVENTORY_ITEM *inv_item); +static bool M_IsFlipping(const INVENTORY_ITEM *inv_item); static void M_FlipLeft(INVENTORY_ITEM *inv_item); static void M_FlipRight(INVENTORY_ITEM *inv_item); static void M_Close(INVENTORY_ITEM *inv_item); @@ -318,6 +320,17 @@ static void M_PlayAnyLevel(INVENTORY_ITEM *const inv_item) } } +static int32_t M_GetCurrentPage(const INVENTORY_ITEM *const inv_item) +{ + const int32_t frame = inv_item->goal_frame - inv_item->open_frame; + return frame % 5 == 0 ? frame / 5 : -1; +} + +static bool M_IsFlipping(const INVENTORY_ITEM *const inv_item) +{ + return M_GetCurrentPage(inv_item) == -1; +} + static void M_FlipLeft(INVENTORY_ITEM *const inv_item) { M_RemoveAllText(); @@ -413,18 +426,19 @@ void Option_Passport_Control(INVENTORY_ITEM *const item, const bool is_busy) InvRing_RemoveAllText(); - const int32_t frame = item->goal_frame - item->open_frame; - const int32_t page = frame % 5 == 0 ? frame / 5 : -1; - const bool is_flipping = page == -1; - if (is_flipping) { + if (M_IsFlipping(item)) { return; } - m_State.current_page = page; + m_State.current_page = M_GetCurrentPage(item); if (m_State.current_page < m_State.active_page) { M_FlipRight(item); + g_Input = (INPUT_STATE) {}; + g_InputDB = (INPUT_STATE) {}; } else if (m_State.current_page > m_State.active_page) { M_FlipLeft(item); + g_Input = (INPUT_STATE) {}; + g_InputDB = (INPUT_STATE) {}; } else { m_State.is_ready = true; M_ShowPage(item); From 3e4017d3380be71aee41c47e422e1fbbc1c6354a Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 19:41:37 +0200 Subject: [PATCH 36/52] game-strings: fix crash with -l/--level --- src/libtrx/game/game_string_table/common.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libtrx/game/game_string_table/common.c b/src/libtrx/game/game_string_table/common.c index 9e40ae432..f44260c6c 100644 --- a/src/libtrx/game/game_string_table/common.c +++ b/src/libtrx/game/game_string_table/common.c @@ -124,7 +124,7 @@ static void M_ApplyLayer( } } - if (gs_level_table != nullptr) { + if (gs_level_table != nullptr && gs_level_table->count != 0) { ASSERT(level->num >= 0); ASSERT(level->num < gs_level_table->count); M_Apply(&gs_level_table->entries[level->num].table); From 9c0c0160df66b5308674d10c4b7f3cbd4a87e9ca Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 19:41:51 +0200 Subject: [PATCH 37/52] game-strings: fix memory leak --- src/libtrx/game/game_string_table/common.c | 8 ++++---- src/libtrx/game/game_string_table/priv.c | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libtrx/game/game_string_table/common.c b/src/libtrx/game/game_string_table/common.c index f44260c6c..3d31c1738 100644 --- a/src/libtrx/game/game_string_table/common.c +++ b/src/libtrx/game/game_string_table/common.c @@ -137,7 +137,7 @@ void GameStringTable_Apply(const GF_LEVEL *const level) Object_ResetNames(); ASSERT(m_GST_Layers != nullptr); for (int32_t i = 0; i < m_GST_Layers->count; i++) { - const GS_FILE *const gs_file = Vector_Get(m_GST_Layers, i); + const GS_FILE *const gs_file = *(GS_FILE **)Vector_Get(m_GST_Layers, i); M_ApplyLayer(level, gs_file); } M_DoObjectAliases(); @@ -145,14 +145,14 @@ void GameStringTable_Apply(const GF_LEVEL *const level) void GameStringTable_Init(void) { - m_GST_Layers = Vector_Create(sizeof(GS_FILE)); + m_GST_Layers = Vector_Create(sizeof(GS_FILE *)); } void GameStringTable_Shutdown(void) { if (m_GST_Layers != nullptr) { for (int32_t i = 0; i < m_GST_Layers->count; i++) { - GS_FILE *const gs_file = Vector_Get(m_GST_Layers, i); + GS_FILE *const gs_file = *(GS_FILE **)Vector_Get(m_GST_Layers, i); GS_File_Free(gs_file); } Vector_Free(m_GST_Layers); @@ -168,6 +168,6 @@ void GameStringTable_Load(const char *const path, const bool load_levels) } GS_FILE *gs_file = GS_File_CreateFromString(data, load_levels); ASSERT(m_GST_Layers != nullptr); - Vector_Add(m_GST_Layers, gs_file); + Vector_Add(m_GST_Layers, &gs_file); Memory_FreePointer(&data); } diff --git a/src/libtrx/game/game_string_table/priv.c b/src/libtrx/game/game_string_table/priv.c index 0b62cc41b..a9bada043 100644 --- a/src/libtrx/game/game_string_table/priv.c +++ b/src/libtrx/game/game_string_table/priv.c @@ -52,4 +52,5 @@ void GS_File_Free(GS_FILE *const gs_file) for (int32_t i = 0; i < GFLT_NUMBER_OF; i++) { M_FreeLevelsTable(&gs_file->level_tables[i]); } + Memory_Free(gs_file); } From 1f89b14a46855347bb9c7d80ae85b43107d9e997 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 26 Apr 2025 19:55:58 +0200 Subject: [PATCH 38/52] docs/tr2: release 1.0.2 --- docs/tr2/CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 511ab2bb4..44a0cb21e 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,4 +1,6 @@ -## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...develop) - ××××-××-×× +## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.2...develop) - ××××-××-×× + +## [1.0.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...tr2-1.0.2) - 2025-04-26 - changed The Golden Mask strings to default to the OG strings file for the main tables (#2847) - fixed Lara voiding if she stops on a tile with a closing door, and the door isn't on a portal (#2848) - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856) From 8b78a7f0010cac536c5070110e486ee7753fc57e Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Sun, 27 Apr 2025 12:19:16 +0100 Subject: [PATCH 39/52] room: fix abyss height check As Lara's position is stored as int32, we need to use the same in the abyss height check function, otherwise internal casting yields invalid results. Resolves #2874. --- docs/tr1/CHANGELOG.md | 1 + docs/tr2/CHANGELOG.md | 1 + src/libtrx/game/rooms/common.c | 2 +- src/libtrx/include/libtrx/game/rooms/common.h | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index 31a10b5a1..e885f60fe 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -40,6 +40,7 @@ - fixed Lara at times ending up in incorrect rooms when using the teleport cheat (#2486, regression from 3.0) - fixed the `/pos` console command reporting the base room number when Lara is actually in a flipped room (#2487, regression from 3.0) - fixed clicks in audio sounds (#2846, regression from 2.0) +- fixed Lara being killed if she enters the void in a level that uses the `disable_floor` sequence in the game flow (#2874, regression from 4.9) - improved bubble appearance (#2672) - improved rendering performance - improved pause exit dialog - it can now be canceled with escape diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 8a5084b21..56eae237f 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -2,6 +2,7 @@ - added aliases to CLI options (`-gold` becomes `-g/--gold`) - added a `--help` CLI option (may not output anything on Windows machines – OS bug) - changed the sound dialog appearance (repositioned, added text labels and arrows) +- fixed Lara being killed if she enters the void in a level that uses the `disable_floor` sequence in the game flow (#2874, regression from 0.10) ## [1.0.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...tr2-1.0.2) - 2025-04-26 - changed The Golden Mask strings to default to the OG strings file for the main tables (#2847) diff --git a/src/libtrx/game/rooms/common.c b/src/libtrx/game/rooms/common.c index b10c7ceb7..0dc525142 100644 --- a/src/libtrx/game/rooms/common.c +++ b/src/libtrx/game/rooms/common.c @@ -550,7 +550,7 @@ void Room_SetAbyssHeight(const int16_t height) CLAMPG(m_AbyssMaxHeight, MAX_HEIGHT - STEP_L); } -bool Room_IsAbyssHeight(const int16_t height) +bool Room_IsAbyssHeight(const int32_t height) { return m_AbyssMinHeight != 0 && height >= m_AbyssMinHeight; } diff --git a/src/libtrx/include/libtrx/game/rooms/common.h b/src/libtrx/include/libtrx/game/rooms/common.h index 1d24b62b7..19539f1ce 100644 --- a/src/libtrx/include/libtrx/game/rooms/common.h +++ b/src/libtrx/include/libtrx/game/rooms/common.h @@ -43,7 +43,7 @@ SECTOR *Room_GetPitSector(const SECTOR *sector, int32_t x, int32_t z); SECTOR *Room_GetSkySector(const SECTOR *sector, int32_t x, int32_t z); void Room_SetAbyssHeight(int16_t height); -bool Room_IsAbyssHeight(int16_t height); +bool Room_IsAbyssHeight(int32_t height); HEIGHT_TYPE Room_GetHeightType(void); int16_t Room_GetHeight(const SECTOR *sector, int32_t x, int32_t y, int32_t z); int16_t Room_GetHeightEx( From cdfb5942c1100a0a7d1c4d7726db0f6d450be2a8 Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Sun, 27 Apr 2025 12:45:08 +0100 Subject: [PATCH 40/52] tr2/data: fix Living Quarters flame deactivation Resolves #2851. --- data/tr2/ship/cfg/TR2X_gameflow.json5 | 1 + data/tr2/ship/data/injections/living_fd.bin | Bin 0 -> 127 bytes docs/tr2/CHANGELOG.md | 1 + docs/tr2/README.md | 1 + 4 files changed, 3 insertions(+) create mode 100644 data/tr2/ship/data/injections/living_fd.bin diff --git a/data/tr2/ship/cfg/TR2X_gameflow.json5 b/data/tr2/ship/cfg/TR2X_gameflow.json5 index 5cfda3031..38a4c9665 100644 --- a/data/tr2/ship/cfg/TR2X_gameflow.json5 +++ b/data/tr2/ship/cfg/TR2X_gameflow.json5 @@ -217,6 +217,7 @@ ], "injections": [ "data/injections/living_deck_goon_sfx.bin", + "data/injections/living_fd.bin", "data/injections/living_pickup_meshes.bin", "data/injections/seaweed_collision.bin", ], diff --git a/data/tr2/ship/data/injections/living_fd.bin b/data/tr2/ship/data/injections/living_fd.bin new file mode 100644 index 0000000000000000000000000000000000000000..d23df437d3ff8dccc1d527bc4a5f1c28b9e06221 GIT binary patch literal 127 zcmWFuitu7&U|?WjU|^WQz`&5rz`#&3$F%1(?;!&Lm;1;3rpRwpQd!U-Bk0a2v!H#- zL@gz)#VS??k1f`G`BLV-xb(neo6>7Z1(V*lwfF0#1%6nsxigAypJ5qG&~l-Z{D$v- j=Wbq^^}F5ihq?aZIejhNmB$@3y_>&2|H^JF!~PWj7L7C@ literal 0 HcmV?d00001 diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 56eae237f..a7e9c6255 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -3,6 +3,7 @@ - added a `--help` CLI option (may not output anything on Windows machines – OS bug) - changed the sound dialog appearance (repositioned, added text labels and arrows) - fixed Lara being killed if she enters the void in a level that uses the `disable_floor` sequence in the game flow (#2874, regression from 0.10) +- fixed flame emitter 23 in room 6 not being deactivated when the lever in room 1 is used (#2851) ## [1.0.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...tr2-1.0.2) - 2025-04-26 - changed The Golden Mask strings to default to the OG strings file for the main tables (#2847) diff --git a/docs/tr2/README.md b/docs/tr2/README.md index 54d53df6a..4916baac5 100644 --- a/docs/tr2/README.md +++ b/docs/tr2/README.md @@ -221,6 +221,7 @@ However, you can easily download them manually from these urls: - fixed the following floor data issues: - **Opera House**: fixed the trigger under item 203 to trigger it rather than item 204 - **Wreck of the Maria Doria**: fixed room 98 not having water + - **Living Quarters** - fixed flame emitter 23 in room 6 not being deactivated when the lever in room 1 is used - **The Deck**: fixed invalid portals between rooms 17 and 104, which could result in Lara seeing enemies in disconnected rooms - **Tibetan Foothills**: added missing triggers for the drawbridge in room 96 (after the flipmap) - **Catacombs of the Talion**: changed some music triggers to pads near the first yeti, and added missing triggers and ladder in room 116 (after the flipmap) From ff86b5e7122d3a0b67b35a8a05b7494b1d26c69a Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Thu, 17 Apr 2025 21:44:32 +0100 Subject: [PATCH 41/52] tr2/objects: use common OBJECT_BOUNDS This updates all relevant TR2 objects to use OBJECT_BOUNDS rather than an arbitrary array of values for testing bounds. The approach is equivalent to TR1. Lara_TestPosition has also been moved to TRX, and kept in Lara's module rather than in items.c, as it's only used by Lara. --- src/libtrx/game/lara/common.c | 50 +++++++++++ src/libtrx/include/libtrx/game/lara/common.h | 1 + .../include/libtrx/game/objects/types.h | 4 +- src/tr1/game/items.c | 43 ---------- src/tr1/game/items.h | 2 - src/tr1/game/lara/common.c | 5 -- src/tr1/game/lara/common.h | 1 - src/tr2/game/items.c | 85 ------------------- src/tr2/game/items.h | 2 - src/tr2/game/objects/general/detonator.c | 48 ++++++----- src/tr2/game/objects/general/flare_item.c | 1 + src/tr2/game/objects/general/keyhole.c | 34 ++++---- src/tr2/game/objects/general/movable_block.c | 33 +++---- src/tr2/game/objects/general/pickup.c | 68 +++++++-------- src/tr2/game/objects/general/pickup.h | 3 +- src/tr2/game/objects/general/puzzle_hole.c | 36 ++++---- src/tr2/game/objects/general/switch.c | 69 ++++++++------- src/tr2/game/objects/general/zipline.c | 35 ++++---- src/tr2/global/types_decomp.h | 7 -- 19 files changed, 224 insertions(+), 303 deletions(-) diff --git a/src/libtrx/game/lara/common.c b/src/libtrx/game/lara/common.c index 2f2274ede..6c9173fc7 100644 --- a/src/libtrx/game/lara/common.c +++ b/src/libtrx/game/lara/common.c @@ -2,6 +2,7 @@ #include "game/const.h" #include "game/item_actions.h" +#include "game/matrix.h" #include "game/rooms/const.h" void Lara_Animate(ITEM *const item) @@ -156,3 +157,52 @@ bool Lara_TestBoundsCollide(const ITEM *const item, const int32_t radius) { return Item_TestBoundsCollide(item, Lara_GetItem(), radius); } + +bool Lara_TestPosition( + const ITEM *const item, const OBJECT_BOUNDS *const bounds) +{ + const ITEM *const lara = Lara_GetItem(); + const XYZ_16 rot = { + .x = lara->rot.x - item->rot.x, + .y = lara->rot.y - item->rot.y, + .z = lara->rot.z - item->rot.z, + }; + const XYZ_32 dist = { + .x = lara->pos.x - item->pos.x, + .y = lara->pos.y - item->pos.y, + .z = lara->pos.z - item->pos.z, + }; + + // clang-format off + if (rot.x < bounds->rot.min.x || + rot.x > bounds->rot.max.x || + rot.y < bounds->rot.min.y || + rot.y > bounds->rot.max.y || + rot.z < bounds->rot.min.z || + rot.z > bounds->rot.max.z + ) { + return false; + } + // clang-format on + + Matrix_PushUnit(); + Matrix_Rot16(item->rot); + const MATRIX *const m = g_MatrixPtr; + const XYZ_32 shift = { + .x = (dist.x * m->_00 + dist.y * m->_10 + dist.z * m->_20) >> W2V_SHIFT, + .y = (dist.x * m->_01 + dist.y * m->_11 + dist.z * m->_21) >> W2V_SHIFT, + .z = (dist.x * m->_02 + dist.y * m->_12 + dist.z * m->_22) >> W2V_SHIFT, + }; + Matrix_Pop(); + + // clang-format off + return ( + shift.x >= bounds->shift.min.x && + shift.x <= bounds->shift.max.x && + shift.y >= bounds->shift.min.y && + shift.y <= bounds->shift.max.y && + shift.z >= bounds->shift.min.z && + shift.z <= bounds->shift.max.z + ); + // clang-format on +} diff --git a/src/libtrx/include/libtrx/game/lara/common.h b/src/libtrx/include/libtrx/game/lara/common.h index 068634600..ae80131aa 100644 --- a/src/libtrx/include/libtrx/game/lara/common.h +++ b/src/libtrx/include/libtrx/game/lara/common.h @@ -15,3 +15,4 @@ void Lara_TakeDamage(int16_t damage, bool hit_status); bool Lara_TestBoundsCollide(const ITEM *item, int32_t radius); void Lara_Push(const ITEM *item, COLL_INFO *coll, bool hit_on, bool big_push); +bool Lara_TestPosition(const ITEM *item, const OBJECT_BOUNDS *bounds); diff --git a/src/libtrx/include/libtrx/game/objects/types.h b/src/libtrx/include/libtrx/game/objects/types.h index 88b289dcd..c22f3c549 100644 --- a/src/libtrx/include/libtrx/game/objects/types.h +++ b/src/libtrx/include/libtrx/game/objects/types.h @@ -38,14 +38,12 @@ typedef struct { bool disable_lighting; } OBJECT_MESH; -#if TR_VERSION == 1 typedef struct { struct { XYZ_16 min; XYZ_16 max; } shift, rot; } OBJECT_BOUNDS; -#endif typedef struct OBJECT { int16_t mesh_count; @@ -66,8 +64,8 @@ typedef struct OBJECT { void (*activate_func)(ITEM *item); void (*handle_flip_func)(ITEM *item, ROOM_FLIP_STATUS flip_status); void (*handle_save_func)(ITEM *item, SAVEGAME_STAGE stage); -#if TR_VERSION == 1 const OBJECT_BOUNDS *(*bounds_func)(void); +#if TR_VERSION == 1 bool (*is_usable_func)(int16_t item_num); #endif diff --git a/src/tr1/game/items.c b/src/tr1/game/items.c index bcc068aa1..69afbf81a 100644 --- a/src/tr1/game/items.c +++ b/src/tr1/game/items.c @@ -196,49 +196,6 @@ bool Item_Test3DRange(int32_t x, int32_t y, int32_t z, int32_t range) && (SQUARE(x) + SQUARE(y) + SQUARE(z) < SQUARE(range)); } -bool Item_TestPosition( - const ITEM *const src_item, const ITEM *const dst_item, - const OBJECT_BOUNDS *const bounds) -{ - const XYZ_16 rot = { - .x = src_item->rot.x - dst_item->rot.x, - .y = src_item->rot.y - dst_item->rot.y, - .z = src_item->rot.z - dst_item->rot.z, - }; - if (rot.x < bounds->rot.min.x || rot.x > bounds->rot.max.x - || rot.y < bounds->rot.min.y || rot.y > bounds->rot.max.y - || rot.z < bounds->rot.min.z || rot.z > bounds->rot.max.z) { - return false; - } - - const XYZ_32 dist = { - .x = src_item->pos.x - dst_item->pos.x, - .y = src_item->pos.y - dst_item->pos.y, - .z = src_item->pos.z - dst_item->pos.z, - }; - - Matrix_PushUnit(); - Matrix_Rot16(dst_item->rot); - MATRIX *mptr = g_MatrixPtr; - const XYZ_32 shift = { - .x = (mptr->_00 * dist.x + mptr->_10 * dist.y + mptr->_20 * dist.z) - >> W2V_SHIFT, - .y = (mptr->_01 * dist.x + mptr->_11 * dist.y + mptr->_21 * dist.z) - >> W2V_SHIFT, - .z = (mptr->_02 * dist.x + mptr->_12 * dist.y + mptr->_22 * dist.z) - >> W2V_SHIFT, - }; - Matrix_Pop(); - - if (shift.x < bounds->shift.min.x || shift.x > bounds->shift.max.x - || shift.y < bounds->shift.min.y || shift.y > bounds->shift.max.y - || shift.z < bounds->shift.min.z || shift.z > bounds->shift.max.z) { - return false; - } - - return true; -} - void Item_AlignPosition(ITEM *src_item, ITEM *dst_item, XYZ_32 *vec) { src_item->rot.x = dst_item->rot.x; diff --git a/src/tr1/game/items.h b/src/tr1/game/items.h index a9bc07b68..1f961833c 100644 --- a/src/tr1/game/items.h +++ b/src/tr1/game/items.h @@ -11,8 +11,6 @@ int16_t Item_Spawn(const ITEM *item, GAME_OBJECT_ID obj_id); bool Item_IsNearItem(const ITEM *item, const XYZ_32 *pos, int32_t distance); bool Item_Test3DRange(int32_t x, int32_t y, int32_t z, int32_t range); -bool Item_TestPosition( - const ITEM *src_item, const ITEM *dst_item, const OBJECT_BOUNDS *bounds); void Item_AlignPosition(ITEM *src_item, ITEM *dst_item, XYZ_32 *vec); bool Item_MovePosition( ITEM *src_item, const ITEM *dst_item, const XYZ_32 *vec, int32_t velocity); diff --git a/src/tr1/game/lara/common.c b/src/tr1/game/lara/common.c index 4e1c95406..ce9755f7a 100644 --- a/src/tr1/game/lara/common.c +++ b/src/tr1/game/lara/common.c @@ -701,11 +701,6 @@ bool Lara_IsNearItem(const XYZ_32 *pos, int32_t distance) return Item_IsNearItem(g_LaraItem, pos, distance); } -bool Lara_TestPosition(const ITEM *item, const OBJECT_BOUNDS *const bounds) -{ - return Item_TestPosition(g_LaraItem, item, bounds); -} - void Lara_AlignPosition(ITEM *item, XYZ_32 *vec) { Item_AlignPosition(g_LaraItem, item, vec); diff --git a/src/tr1/game/lara/common.h b/src/tr1/game/lara/common.h index 291ab20e9..e047b7291 100644 --- a/src/tr1/game/lara/common.h +++ b/src/tr1/game/lara/common.h @@ -26,7 +26,6 @@ void Lara_SwapMeshExtra(void); bool Lara_IsNearItem(const XYZ_32 *pos, int32_t distance); void Lara_UseItem(GAME_OBJECT_ID obj_id); -bool Lara_TestPosition(const ITEM *item, const OBJECT_BOUNDS *bounds); void Lara_AlignPosition(ITEM *item, XYZ_32 *vec); bool Lara_MovePosition(ITEM *item, XYZ_32 *vec); diff --git a/src/tr2/game/items.c b/src/tr2/game/items.c index a66bb7eb3..92df663c4 100644 --- a/src/tr2/game/items.c +++ b/src/tr2/game/items.c @@ -20,40 +20,6 @@ static BOUNDS_16 m_NullBounds = {}; static BOUNDS_16 m_InterpolatedBounds = {}; -static OBJECT_BOUNDS M_ConvertBounds(const int16_t *bounds_in); - -static OBJECT_BOUNDS M_ConvertBounds(const int16_t *const bounds_in) -{ - // TODO: remove this conversion utility once we gain control over its - // incoming arguments - return (OBJECT_BOUNDS) { - .shift = { - .min = { - .x = bounds_in[0], - .y = bounds_in[2], - .z = bounds_in[4], - }, - .max = { - .x = bounds_in[1], - .y = bounds_in[3], - .z = bounds_in[5], - }, - }, - .rot = { - .min = { - .x = bounds_in[6], - .y = bounds_in[8], - .z = bounds_in[10], - }, - .max = { - .x = bounds_in[7], - .y = bounds_in[9], - .z = bounds_in[11], - }, - }, - }; -} - void Item_Control(void) { int16_t item_num = Item_GetNextActive(); @@ -188,57 +154,6 @@ int16_t Item_GetHeight(const ITEM *const item) return height; } -int32_t Item_TestPosition( - const int16_t *const bounds_in, const ITEM *const src_item, - const ITEM *const dst_item) -{ - const OBJECT_BOUNDS bounds = M_ConvertBounds(bounds_in); - - const XYZ_16 rot = { - .x = dst_item->rot.x - src_item->rot.x, - .y = dst_item->rot.y - src_item->rot.y, - .z = dst_item->rot.z - src_item->rot.z, - }; - const XYZ_32 dist = { - .x = dst_item->pos.x - src_item->pos.x, - .y = dst_item->pos.y - src_item->pos.y, - .z = dst_item->pos.z - src_item->pos.z, - }; - - // clang-format off - if (rot.x < bounds.rot.min.x || - rot.x > bounds.rot.max.x || - rot.y < bounds.rot.min.y || - rot.y > bounds.rot.max.y || - rot.z < bounds.rot.min.z || - rot.z > bounds.rot.max.z - ) { - return false; - } - // clang-format on - - Matrix_PushUnit(); - Matrix_Rot16(src_item->rot); - const MATRIX *const m = g_MatrixPtr; - const XYZ_32 shift = { - .x = (dist.x * m->_00 + dist.y * m->_10 + dist.z * m->_20) >> W2V_SHIFT, - .y = (dist.x * m->_01 + dist.y * m->_11 + dist.z * m->_21) >> W2V_SHIFT, - .z = (dist.x * m->_02 + dist.y * m->_12 + dist.z * m->_22) >> W2V_SHIFT, - }; - Matrix_Pop(); - - // clang-format off - return ( - shift.x >= bounds.shift.min.x && - shift.x <= bounds.shift.max.x && - shift.y >= bounds.shift.min.y && - shift.y <= bounds.shift.max.y && - shift.z >= bounds.shift.min.z && - shift.z <= bounds.shift.max.z - ); - // clang-format on -} - void Item_AlignPosition( const XYZ_32 *const vec, const ITEM *const src_item, ITEM *const dst_item) { diff --git a/src/tr2/game/items.h b/src/tr2/game/items.h index 6a26aa675..cb24b329a 100644 --- a/src/tr2/game/items.h +++ b/src/tr2/game/items.h @@ -8,8 +8,6 @@ void Item_Control(void); void Item_ClearKilled(void); void Item_ShiftCol(ITEM *item, COLL_INFO *coll); void Item_UpdateRoom(ITEM *item, int32_t height); -int32_t Item_TestPosition( - const int16_t *bounds, const ITEM *src_item, const ITEM *dst_item); void Item_AlignPosition( const XYZ_32 *vec, const ITEM *src_item, ITEM *dst_item); int32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frmptr[], int32_t *rate); diff --git a/src/tr2/game/objects/general/detonator.c b/src/tr2/game/objects/general/detonator.c index 50a4980e6..ec84db708 100644 --- a/src/tr2/game/objects/general/detonator.c +++ b/src/tr2/game/objects/general/detonator.c @@ -10,33 +10,37 @@ #include "game/sound.h" #include "global/vars.h" +#include + #define EXPLOSION_START_FRAME 76 #define EXPLOSION_END_FRAME 99 #define EXPLOSION_ACTION_FRAME 80 static XYZ_32 m_DetonatorPosition = { .x = 0, .y = 0, .z = 0 }; -static int16_t m_GongBounds[12] = { - -WALL_L / 2, - +WALL_L, - -100, - +100, - -WALL_L / 2 - 300, - -WALL_L / 2 + 100, - -30 * DEG_1, - +30 * DEG_1, - +0, - +0, - +0, - +0, +static const OBJECT_BOUNDS m_GongBounds = { + .shift = { + .min = { .x = -WALL_L / 2, .y = -100, .z = -WALL_L / 2 - 300, }, + .max = { .x = +WALL_L, .y = +100, .z = -WALL_L / 2 + 100, }, + }, + .rot = { + .min = { .x = -30 * DEG_1, .y = 0, .z = 0, }, + .max = { .x = +30 * DEG_1, .y = 0, .z = 0, }, + }, }; +static const OBJECT_BOUNDS *M_Bounds(void); static void M_CreateGongBonger(ITEM *lara_item); static void M_Setup1(OBJECT *obj); static void M_Setup2(OBJECT *obj); static void M_Control(int16_t item_num); static void M_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); +static const OBJECT_BOUNDS *M_Bounds(void) +{ + return &m_GongBounds; +} + static void M_CreateGongBonger(ITEM *const lara_item) { const int16_t item_gong_bonger_num = Item_Create(); @@ -64,12 +68,14 @@ static void M_CreateGongBonger(ITEM *const lara_item) static void M_Setup1(OBJECT *const obj) { obj->collision_func = M_Collision; + obj->bounds_func = M_Bounds; } static void M_Setup2(OBJECT *const obj) { obj->collision_func = M_Collision; obj->control_func = M_Control; + obj->bounds_func = Pickup_Bounds; obj->save_flags = 1; obj->save_anim = 1; } @@ -102,6 +108,7 @@ static void M_Collision( } ITEM *const item = Item_Get(item_num); + const OBJECT *const obj = Object_Get(item->object_id); const XYZ_16 old_rot = item->rot; const int16_t x = item->rot.x; const int16_t y = item->rot.y; @@ -117,16 +124,11 @@ static void M_Collision( goto normal_collision; } - if (item->object_id == O_DETONATOR_2) { - if (!Item_TestPosition(g_PickupBounds, item, lara_item)) { - goto normal_collision; - } - } else { - if (!Item_TestPosition(m_GongBounds, item, lara_item)) { - goto normal_collision; - } else { - item->rot = old_rot; - } + if (!Lara_TestPosition(item, obj->bounds_func())) { + goto normal_collision; + } + if (item->object_id == O_DETONATOR_1) { + item->rot = old_rot; } if (g_Inv_Chosen == NO_OBJECT) { diff --git a/src/tr2/game/objects/general/flare_item.c b/src/tr2/game/objects/general/flare_item.c index 0a89767cb..fe893694e 100644 --- a/src/tr2/game/objects/general/flare_item.c +++ b/src/tr2/game/objects/general/flare_item.c @@ -6,6 +6,7 @@ static void M_Setup(OBJECT *obj); static void M_Setup(OBJECT *const obj) { obj->collision_func = Pickup_Collision; + obj->bounds_func = Pickup_Bounds; obj->control_func = Flare_Control; obj->draw_func = Flare_DrawInAir; obj->save_position = 1; diff --git a/src/tr2/game/objects/general/keyhole.c b/src/tr2/game/objects/general/keyhole.c index 359d23110..c564cd3df 100644 --- a/src/tr2/game/objects/general/keyhole.c +++ b/src/tr2/game/objects/general/keyhole.c @@ -17,29 +17,29 @@ static XYZ_32 m_KeyholePosition = { .z = WALL_L / 2 - LARA_RADIUS - 50, }; -static int16_t m_KeyholeBounds[12] = { - // clang-format off - -200, - +200, - +0, - +0, - +WALL_L / 2 - 200, - +WALL_L / 2, - -10 * DEG_1, - +10 * DEG_1, - -30 * DEG_1, - +30 * DEG_1, - -10 * DEG_1, - +10 * DEG_1, - // clang-format on +static const OBJECT_BOUNDS m_KeyHoleBounds = { + .shift = { + .min = { .x = -200, .y = +0, .z = +WALL_L / 2 - 200, }, + .max = { .x = +200, .y = +0, .z = +WALL_L / 2, }, + }, + .rot = { + .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, }, + .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, }, + }, }; +static const OBJECT_BOUNDS *M_Bounds(void); static void M_Consume( ITEM *lara_item, ITEM *keyhole_item, GAME_OBJECT_ID key_obj_id); static void M_Refuse(const ITEM *lara_item); static void M_Setup(OBJECT *obj); static void M_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); +static const OBJECT_BOUNDS *M_Bounds(void) +{ + return &m_KeyHoleBounds; +} + static void M_Refuse(const ITEM *const lara_item) { if (lara_item->pos.x == g_InteractPosition.x @@ -71,6 +71,7 @@ static void M_Consume( static void M_Setup(OBJECT *const obj) { obj->collision_func = M_Collision; + obj->bounds_func = M_Bounds; obj->save_flags = 1; } @@ -82,12 +83,13 @@ static void M_Collision( } ITEM *const item = Item_Get(item_num); + const OBJECT *const obj = Object_Get(item->object_id); if ((g_Inv_Chosen == NO_OBJECT && !g_Input.action) || g_Lara.gun_status != LGS_ARMLESS || lara_item->gravity) { return; } - if (!Item_TestPosition(m_KeyholeBounds, item, lara_item)) { + if (!Lara_TestPosition(item, obj->bounds_func())) { return; } diff --git a/src/tr2/game/objects/general/movable_block.c b/src/tr2/game/objects/general/movable_block.c index 63f0f1b2b..3dbea88e8 100644 --- a/src/tr2/game/objects/general/movable_block.c +++ b/src/tr2/game/objects/general/movable_block.c @@ -20,21 +20,18 @@ typedef enum { MOVABLE_BLOCK_STATE_PULL = 3, } MOVABLE_BLOCK_STATE; -static int16_t m_MovableBlockBounds[12] = { - -300, - +300, - +0, - +0, - -WALL_L / 2 - LARA_RADIUS - 80, - -WALL_L / 2, - -10 * DEG_1, - +10 * DEG_1, - -30 * DEG_1, - +30 * DEG_1, - -10 * DEG_1, - +10 * DEG_1, +static const OBJECT_BOUNDS m_MovableBlockBounds = { + .shift = { + .min = { .x = -300, .y = 0, .z = -WALL_L / 2 - LARA_RADIUS - 80, }, + .max = { .x = +300, .y = 0, .z = -WALL_L / 2, }, + }, + .rot = { + .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, }, + .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, }, + }, }; +static const OBJECT_BOUNDS *M_Bounds(void); static bool M_TestDestination(const ITEM *item, int32_t block_height); static bool M_TestPush( const ITEM *item, int32_t block_height, DIRECTION quadrant); @@ -47,6 +44,11 @@ static void M_Draw(const ITEM *item); static void M_Control(int16_t item_num); static void M_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); +static const OBJECT_BOUNDS *M_Bounds(void) +{ + return &m_MovableBlockBounds; +} + static bool M_TestDestination( const ITEM *const item, const int32_t block_height) { @@ -199,6 +201,7 @@ static void M_Setup(OBJECT *const obj) obj->handle_save_func = M_HandleSave; obj->control_func = M_Control; obj->collision_func = M_Collision; + obj->bounds_func = M_Bounds; obj->draw_func = M_Draw; obj->save_position = 1; obj->save_flags = 1; @@ -303,7 +306,7 @@ static void M_Collision( break; } - if (!Item_TestPosition(m_MovableBlockBounds, item, lara_item)) { + if (!Lara_TestPosition(item, obj->bounds_func())) { return; } @@ -346,7 +349,7 @@ static void M_Collision( } else if ( Item_TestAnimEqual(lara_item, LA_PUSHABLE_GRAB) && Item_TestFrameEqual(lara_item, LF_PPREADY)) { - if (!Item_TestPosition(m_MovableBlockBounds, item, lara_item)) { + if (!Lara_TestPosition(item, obj->bounds_func())) { return; } diff --git a/src/tr2/game/objects/general/pickup.c b/src/tr2/game/objects/general/pickup.c index 5d0910541..dab94658f 100644 --- a/src/tr2/game/objects/general/pickup.c +++ b/src/tr2/game/objects/general/pickup.c @@ -26,41 +26,29 @@ #define LF_PICKUP_FLARE_UW 20 #define LF_PICKUP_UW 18 -int16_t g_PickupBounds[12] = { - // clang-format off - -WALL_L / 4, - +WALL_L / 4, - -100, - +100, - -WALL_L / 4, - +WALL_L / 4, - -10 * DEG_1, - +10 * DEG_1, - +0, - +0, - +0, - +0, - // clang-format on -}; - static XYZ_32 m_PickupPosition = { .x = 0, .y = 0, .z = -100 }; static XYZ_32 m_PickupPositionUW = { .x = 0, .y = -200, .z = -350 }; -static int16_t m_PickupBoundsUW[12] = { - // clang-format off - -WALL_L / 2, - +WALL_L / 2, - -WALL_L / 2, - +WALL_L / 2, - -WALL_L / 2, - +WALL_L / 2, - -45 * DEG_1, - +45 * DEG_1, - -45 * DEG_1, - +45 * DEG_1, - -45 * DEG_1, - +45 * DEG_1, - // clang-format on +static const OBJECT_BOUNDS m_PickUpBounds = { + .shift = { + .min = { .x = -WALL_L / 4, .y = -100, .z = -WALL_L / 4, }, + .max = { .x = +WALL_L / 4, .y = +100, .z = +WALL_L / 4, }, + }, + .rot = { + .min = { .x = -10 * DEG_1, .y = 0, .z = 0, }, + .max = { .x = +10 * DEG_1, .y = 0, .z = 0, }, + }, +}; + +static const OBJECT_BOUNDS m_PickUpBoundsUW = { + .shift = { + .min = { .x = -WALL_L / 2, .y = -WALL_L / 2, .z = -WALL_L / 2, }, + .max = { .x = +WALL_L / 2, .y = +WALL_L / 2, .z = +WALL_L / 2, }, + }, + .rot = { + .min = { .x = -45 * DEG_1, .y = -45 * DEG_1, .z = -45 * DEG_1, }, + .max = { .x = +45 * DEG_1, .y = +45 * DEG_1, .z = +45 * DEG_1, }, + }, }; static void M_DoPickup(int16_t item_num); @@ -107,13 +95,14 @@ static void M_DoFlarePickup(const int16_t item_num) static void M_DoAboveWater(const int16_t item_num, ITEM *const lara_item) { ITEM *const item = Item_Get(item_num); + const OBJECT *const obj = Object_Get(item->object_id); const XYZ_16 old_rot = item->rot; item->rot.x = 0; item->rot.y = lara_item->rot.y; item->rot.z = 0; - if (!Item_TestPosition(g_PickupBounds, item, lara_item)) { + if (!Lara_TestPosition(item, obj->bounds_func())) { goto cleanup; } @@ -163,13 +152,14 @@ cleanup: static void M_DoUnderwater(const int16_t item_num, ITEM *const lara_item) { ITEM *const item = Item_Get(item_num); + const OBJECT *const obj = Object_Get(item->object_id); const XYZ_16 old_rot = item->rot; item->rot.x = -25 * DEG_1; item->rot.y = lara_item->rot.y; item->rot.z = 0; - if (!Item_TestPosition(m_PickupBoundsUW, item, lara_item)) { + if (!Lara_TestPosition(item, obj->bounds_func())) { goto cleanup; } @@ -224,6 +214,7 @@ static void M_Setup(OBJECT *const obj) obj->handle_save_func = M_HandleSave; obj->activate_func = M_Activate; obj->collision_func = Pickup_Collision; + obj->bounds_func = Pickup_Bounds; obj->draw_func = M_Draw; obj->save_position = 1; obj->save_flags = 1; @@ -343,6 +334,15 @@ static void M_Draw(const ITEM *const item) Matrix_Pop(); } +const OBJECT_BOUNDS *Pickup_Bounds(void) +{ + if (g_Lara.water_status == LWS_UNDERWATER) { + return &m_PickUpBoundsUW; + } else { + return &m_PickUpBounds; + } +} + void Pickup_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { diff --git a/src/tr2/game/objects/general/pickup.h b/src/tr2/game/objects/general/pickup.h index dfac91fc3..1b1a51fff 100644 --- a/src/tr2/game/objects/general/pickup.h +++ b/src/tr2/game/objects/general/pickup.h @@ -2,7 +2,6 @@ #include "global/types.h" -extern int16_t g_PickupBounds[]; - void Pickup_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); bool Pickup_Trigger(int16_t item_num); +const OBJECT_BOUNDS *Pickup_Bounds(void); diff --git a/src/tr2/game/objects/general/puzzle_hole.c b/src/tr2/game/objects/general/puzzle_hole.c index 174755780..45aa8fe52 100644 --- a/src/tr2/game/objects/general/puzzle_hole.c +++ b/src/tr2/game/objects/general/puzzle_hole.c @@ -17,23 +17,18 @@ static XYZ_32 m_PuzzleHolePosition = { .z = WALL_L / 2 - LARA_RADIUS - 85, }; -static int16_t m_PuzzleHoleBounds[12] = { - // clang-format off - -200, - +200, - +0, - +0, - +WALL_L / 2 - 200, - +WALL_L / 2, - -10 * DEG_1, - +10 * DEG_1, - -30 * DEG_1, - +30 * DEG_1, - -10 * DEG_1, - +10 * DEG_1, - // clang-format on +static const OBJECT_BOUNDS m_PuzzleHoleBounds = { + .shift = { + .min = { .x = -200, .y = 0, .z = WALL_L / 2 - 200, }, + .max = { .x = +200, .y = 0, .z = WALL_L / 2, }, + }, + .rot = { + .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, }, + .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, }, + }, }; +static const OBJECT_BOUNDS *M_Bounds(void); static void M_Refuse(const ITEM *lara_item); static void M_Consume( ITEM *lara_item, ITEM *puzzle_hole_item, GAME_OBJECT_ID puzzle_obj_id); @@ -43,6 +38,11 @@ static void M_SetupDone(OBJECT *obj); static void M_HandleSave(ITEM *item, SAVEGAME_STAGE stage); static void M_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); +static const OBJECT_BOUNDS *M_Bounds(void) +{ + return &m_PuzzleHoleBounds; +} + static void M_Refuse(const ITEM *const lara_item) { if (lara_item->pos.x != g_InteractPosition.x @@ -82,6 +82,7 @@ static void M_SetupEmpty(OBJECT *const obj) { obj->collision_func = M_Collision; obj->handle_save_func = M_HandleSave; + obj->bounds_func = M_Bounds; obj->save_flags = 1; } @@ -103,10 +104,11 @@ static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); + const OBJECT *const obj = Object_Get(item->object_id); if (lara_item->current_anim_state != LS_STOP) { if (lara_item->current_anim_state != LS_USE_PUZZLE - || !Item_TestPosition(m_PuzzleHoleBounds, item, lara_item) + || !Lara_TestPosition(item, obj->bounds_func()) || !Item_TestFrameEqual(lara_item, LF_USE_PUZZLE)) { return; } @@ -120,7 +122,7 @@ static void M_Collision( return; } - if (!Item_TestPosition(m_PuzzleHoleBounds, item, lara_item)) { + if (!Lara_TestPosition(item, obj->bounds_func())) { return; } diff --git a/src/tr2/game/objects/general/switch.c b/src/tr2/game/objects/general/switch.c index 3af8f70e2..2053d2a4e 100644 --- a/src/tr2/game/objects/general/switch.c +++ b/src/tr2/game/objects/general/switch.c @@ -17,40 +17,30 @@ static XYZ_32 g_PushSwitchPosition = { .x = 0, .y = 0, .z = 292 }; static XYZ_32 m_AirlockPosition = { .x = 0, .y = 0, .z = 212 }; static XYZ_32 m_SwitchUWPosition = { .x = 0, .y = 0, .z = 108 }; -static int16_t m_SwitchBounds[12] = { - // clang-format off - -220, - +220, - +0, - +0, - +WALL_L / 2 - 220, - +WALL_L / 2, - -10 * DEG_1, - +10 * DEG_1, - -30 * DEG_1, - +30 * DEG_1, - -10 * DEG_1, - +10 * DEG_1, - // clang-format on +static const OBJECT_BOUNDS m_SwitchBounds = { + .shift = { + .min = { .x = -220, .y = +0, .z = +WALL_L / 2 - 220, }, + .max = { .x = +220, .y = +0, .z = +WALL_L / 2, }, + }, + .rot = { + .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, }, + .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, }, + }, }; -static int16_t m_SwitchBoundsUW[12] = { - // clang-format off - -WALL_L, - +WALL_L, - -WALL_L, - +WALL_L, - -WALL_L, - +WALL_L / 2, - -80 * DEG_1, - +80 * DEG_1, - -80 * DEG_1, - +80 * DEG_1, - -80 * DEG_1, - +80 * DEG_1, - // clang-format on +static const OBJECT_BOUNDS m_SwitchBoundsUW = { + .shift = { + .min = { .x = -WALL_L, .y = -WALL_L, .z = -WALL_L, }, + .max = { .x = +WALL_L, .y = +WALL_L, .z = +WALL_L / 2, }, + }, + .rot = { + .min = { .x = -80 * DEG_1, .y = -80 * DEG_1, .z = -80 * DEG_1, }, + .max = { .x = +80 * DEG_1, .y = +80 * DEG_1, .z = +80 * DEG_1, }, + }, }; +static const OBJECT_BOUNDS *M_Bounds(void); +static const OBJECT_BOUNDS *M_BoundsUW(void); static void M_AlignLara(ITEM *lara_item, ITEM *switch_item); static void M_SwitchOn(ITEM *switch_item, ITEM *lara_item); static void M_SwitchOff(ITEM *switch_item, ITEM *lara_item); @@ -62,6 +52,16 @@ static void M_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); static void M_CollisionUW(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); static void M_Control(int16_t item_num); +static const OBJECT_BOUNDS *M_Bounds(void) +{ + return &m_SwitchBounds; +} + +static const OBJECT_BOUNDS *M_BoundsUW(void) +{ + return &m_SwitchBoundsUW; +} + static void M_AlignLara(ITEM *const lara_item, ITEM *const switch_item) { switch (switch_item->object_id) { @@ -139,28 +139,32 @@ static void M_Setup(OBJECT *const obj) { M_SetupBase(obj); obj->collision_func = M_Collision; + obj->bounds_func = M_Bounds; } static void M_SetupPushButton(OBJECT *const obj) { M_Setup(obj); obj->enable_interpolation = false; + obj->bounds_func = M_Bounds; } static void M_SetupUW(OBJECT *const obj) { M_SetupBase(obj); obj->collision_func = M_CollisionUW; + obj->bounds_func = M_BoundsUW; } static void M_Collision( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); + const OBJECT *const obj = Object_Get(item->object_id); if (!g_Input.action || item->status != IS_INACTIVE || g_Lara.gun_status != LGS_ARMLESS || lara_item->gravity || lara_item->current_anim_state != LS_STOP - || !Item_TestPosition(m_SwitchBounds, item, lara_item)) { + || !Lara_TestPosition(item, obj->bounds_func())) { return; } @@ -193,6 +197,7 @@ static void M_CollisionUW( const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) { ITEM *const item = Item_Get(item_num); + const OBJECT *const obj = Object_Get(item->object_id); if (!g_Input.action || item->status != IS_INACTIVE || g_Lara.water_status != LWS_UNDERWATER @@ -201,7 +206,7 @@ static void M_CollisionUW( return; } - if (!Item_TestPosition(m_SwitchBoundsUW, item, lara_item)) { + if (!Lara_TestPosition(item, obj->bounds_func())) { return; } diff --git a/src/tr2/game/objects/general/zipline.c b/src/tr2/game/objects/general/zipline.c index 6dd04d867..447cfa4fa 100644 --- a/src/tr2/game/objects/general/zipline.c +++ b/src/tr2/game/objects/general/zipline.c @@ -22,33 +22,35 @@ static XYZ_32 m_ZiplineHandlePosition = { .y = 0, .z = WALL_L / 2 - 141, }; -static int16_t m_ZiplineHandleBounds[12] = { - // clang-format off - -WALL_L / 4, - +WALL_L / 4, - -100, - +100, - +WALL_L / 4, - +WALL_L / 2, - +0, - +0, - -25 * DEG_1, - +25 * DEG_1, - +0, - +0, - // clang-format on + +static const OBJECT_BOUNDS m_ZiplineHandleBounds = { + .shift = { + .min = { .x = -WALL_L / 4, .y = -100, .z = +WALL_L / 4, }, + .max = { .x = +WALL_L / 4, .y = +100, .z = +WALL_L / 2, }, + }, + .rot = { + .min = { .x = +0, .y = -25 * DEG_1, .z = +0, }, + .max = { .x = +0, .y = +25 * DEG_1, .z = +0, }, + }, }; +static const OBJECT_BOUNDS *M_Bounds(void); static void M_Setup(OBJECT *obj); static void M_Initialise(int16_t item_num); static void M_Control(int16_t item_num); static void M_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); +static const OBJECT_BOUNDS *M_Bounds(void) +{ + return &m_ZiplineHandleBounds; +} + static void M_Setup(OBJECT *const obj) { obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = M_Collision; + obj->bounds_func = M_Bounds; obj->save_position = 1; obj->save_flags = 1; obj->save_anim = 1; @@ -149,7 +151,8 @@ static void M_Collision( return; } - if (!Item_TestPosition(m_ZiplineHandleBounds, item, lara_item)) { + const OBJECT *const obj = Object_Get(item->object_id); + if (!Lara_TestPosition(item, obj->bounds_func())) { return; } diff --git a/src/tr2/global/types_decomp.h b/src/tr2/global/types_decomp.h index 3d1825df8..59ecb25d0 100644 --- a/src/tr2/global/types_decomp.h +++ b/src/tr2/global/types_decomp.h @@ -151,13 +151,6 @@ typedef struct { int32_t pitch; } SKIDOO_INFO; -typedef struct { - struct { - XYZ_16 min; - XYZ_16 max; - } shift, rot; -} OBJECT_BOUNDS; - typedef struct { int32_t xv; int32_t yv; From e03c65ca0f7de377255489702e230e15253d6212 Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Sun, 27 Apr 2025 15:30:52 +0100 Subject: [PATCH 42/52] lara/common: move Lara_AlignPosition to TRX This merges Lara_AlignPosition (Item_AlignPosition) into TRX. --- src/libtrx/game/lara/common.c | 42 +++++++++++++++++++- src/libtrx/include/libtrx/game/lara/common.h | 1 + src/tr1/game/items.c | 21 ---------- src/tr1/game/items.h | 1 - src/tr1/game/lara/common.c | 5 --- src/tr1/game/lara/common.h | 1 - src/tr2/game/items.c | 38 ------------------ src/tr2/game/items.h | 2 - src/tr2/game/objects/general/detonator.c | 2 +- src/tr2/game/objects/general/keyhole.c | 2 +- src/tr2/game/objects/general/pickup.c | 2 +- src/tr2/game/objects/general/puzzle_hole.c | 2 +- src/tr2/game/objects/general/switch.c | 6 +-- src/tr2/game/objects/general/zipline.c | 2 +- 14 files changed, 50 insertions(+), 77 deletions(-) diff --git a/src/libtrx/game/lara/common.c b/src/libtrx/game/lara/common.c index 6c9173fc7..eded61e70 100644 --- a/src/libtrx/game/lara/common.c +++ b/src/libtrx/game/lara/common.c @@ -2,8 +2,9 @@ #include "game/const.h" #include "game/item_actions.h" +#include "game/lara/const.h" #include "game/matrix.h" -#include "game/rooms/const.h" +#include "game/rooms.h" void Lara_Animate(ITEM *const item) { @@ -206,3 +207,42 @@ bool Lara_TestPosition( ); // clang-format on } + +void Lara_AlignPosition(const ITEM *const item, const XYZ_32 *const vec) +{ + ITEM *const lara = Lara_GetItem(); + lara->rot = item->rot; + Matrix_PushUnit(); + Matrix_Rot16(item->rot); + const MATRIX *const m = g_MatrixPtr; + const XYZ_32 shift = { + .x = (vec->x * m->_00 + vec->y * m->_01 + vec->z * m->_02) >> W2V_SHIFT, + .y = (vec->x * m->_10 + vec->y * m->_11 + vec->z * m->_12) >> W2V_SHIFT, + .z = (vec->x * m->_20 + vec->y * m->_21 + vec->z * m->_22) >> W2V_SHIFT, + }; + Matrix_Pop(); + + const XYZ_32 new_pos = { + .x = item->pos.x + shift.x, + .y = item->pos.y + shift.y, + .z = item->pos.z + shift.z, + }; + +#if TR_VERSION == 2 + // TODO: check the significance of this in TR1 + int16_t room_num = lara->room_num; + const SECTOR *const sector = + Room_GetSector(new_pos.x, new_pos.y, new_pos.z, &room_num); + const int32_t height = + Room_GetHeight(sector, new_pos.x, new_pos.y, new_pos.z); + const int32_t ceiling = + Room_GetCeiling(sector, new_pos.x, new_pos.y, new_pos.z); + + if (ABS(height - lara->pos.y) > STEP_L + || ABS(ceiling - lara->pos.y) < LARA_HEIGHT) { + return; + } +#endif + + lara->pos = new_pos; +} diff --git a/src/libtrx/include/libtrx/game/lara/common.h b/src/libtrx/include/libtrx/game/lara/common.h index ae80131aa..82e2f7357 100644 --- a/src/libtrx/include/libtrx/game/lara/common.h +++ b/src/libtrx/include/libtrx/game/lara/common.h @@ -16,3 +16,4 @@ void Lara_TakeDamage(int16_t damage, bool hit_status); bool Lara_TestBoundsCollide(const ITEM *item, int32_t radius); void Lara_Push(const ITEM *item, COLL_INFO *coll, bool hit_on, bool big_push); bool Lara_TestPosition(const ITEM *item, const OBJECT_BOUNDS *bounds); +void Lara_AlignPosition(const ITEM *item, const XYZ_32 *vec); diff --git a/src/tr1/game/items.c b/src/tr1/game/items.c index 69afbf81a..fdb607cbb 100644 --- a/src/tr1/game/items.c +++ b/src/tr1/game/items.c @@ -196,27 +196,6 @@ bool Item_Test3DRange(int32_t x, int32_t y, int32_t z, int32_t range) && (SQUARE(x) + SQUARE(y) + SQUARE(z) < SQUARE(range)); } -void Item_AlignPosition(ITEM *src_item, ITEM *dst_item, XYZ_32 *vec) -{ - src_item->rot.x = dst_item->rot.x; - src_item->rot.y = dst_item->rot.y; - src_item->rot.z = dst_item->rot.z; - - Matrix_PushUnit(); - Matrix_Rot16(dst_item->rot); - MATRIX *mptr = g_MatrixPtr; - src_item->pos.x = dst_item->pos.x - + ((mptr->_00 * vec->x + mptr->_01 * vec->y + mptr->_02 * vec->z) - >> W2V_SHIFT); - src_item->pos.y = dst_item->pos.y - + ((mptr->_10 * vec->x + mptr->_11 * vec->y + mptr->_12 * vec->z) - >> W2V_SHIFT); - src_item->pos.z = dst_item->pos.z - + ((mptr->_20 * vec->x + mptr->_21 * vec->y + mptr->_22 * vec->z) - >> W2V_SHIFT); - Matrix_Pop(); -} - bool Item_MovePosition( ITEM *item, const ITEM *ref_item, const XYZ_32 *vec, int32_t velocity) { diff --git a/src/tr1/game/items.h b/src/tr1/game/items.h index 1f961833c..c94893dfc 100644 --- a/src/tr1/game/items.h +++ b/src/tr1/game/items.h @@ -11,7 +11,6 @@ int16_t Item_Spawn(const ITEM *item, GAME_OBJECT_ID obj_id); bool Item_IsNearItem(const ITEM *item, const XYZ_32 *pos, int32_t distance); bool Item_Test3DRange(int32_t x, int32_t y, int32_t z, int32_t range); -void Item_AlignPosition(ITEM *src_item, ITEM *dst_item, XYZ_32 *vec); bool Item_MovePosition( ITEM *src_item, const ITEM *dst_item, const XYZ_32 *vec, int32_t velocity); void Item_ShiftCol(ITEM *item, COLL_INFO *coll); diff --git a/src/tr1/game/lara/common.c b/src/tr1/game/lara/common.c index ce9755f7a..6596d42fc 100644 --- a/src/tr1/game/lara/common.c +++ b/src/tr1/game/lara/common.c @@ -701,11 +701,6 @@ bool Lara_IsNearItem(const XYZ_32 *pos, int32_t distance) return Item_IsNearItem(g_LaraItem, pos, distance); } -void Lara_AlignPosition(ITEM *item, XYZ_32 *vec) -{ - Item_AlignPosition(g_LaraItem, item, vec); -} - bool Lara_MovePosition(ITEM *item, XYZ_32 *vec) { int32_t velocity = g_Config.gameplay.enable_walk_to_items diff --git a/src/tr1/game/lara/common.h b/src/tr1/game/lara/common.h index e047b7291..620b0c31e 100644 --- a/src/tr1/game/lara/common.h +++ b/src/tr1/game/lara/common.h @@ -26,7 +26,6 @@ void Lara_SwapMeshExtra(void); bool Lara_IsNearItem(const XYZ_32 *pos, int32_t distance); void Lara_UseItem(GAME_OBJECT_ID obj_id); -void Lara_AlignPosition(ITEM *item, XYZ_32 *vec); bool Lara_MovePosition(ITEM *item, XYZ_32 *vec); void Lara_RevertToPistolsIfNeeded(void); diff --git a/src/tr2/game/items.c b/src/tr2/game/items.c index 92df663c4..09c7b16ca 100644 --- a/src/tr2/game/items.c +++ b/src/tr2/game/items.c @@ -154,44 +154,6 @@ int16_t Item_GetHeight(const ITEM *const item) return height; } -void Item_AlignPosition( - const XYZ_32 *const vec, const ITEM *const src_item, ITEM *const dst_item) -{ - dst_item->rot = src_item->rot; - Matrix_PushUnit(); - Matrix_Rot16(src_item->rot); - const MATRIX *const m = g_MatrixPtr; - const XYZ_32 shift = { - .x = (vec->x * m->_00 + vec->y * m->_01 + vec->z * m->_02) >> W2V_SHIFT, - .y = (vec->x * m->_10 + vec->y * m->_11 + vec->z * m->_12) >> W2V_SHIFT, - .z = (vec->x * m->_20 + vec->y * m->_21 + vec->z * m->_22) >> W2V_SHIFT, - }; - Matrix_Pop(); - - const XYZ_32 new_pos = { - .x = src_item->pos.x + shift.x, - .y = src_item->pos.y + shift.y, - .z = src_item->pos.z + shift.z, - }; - - int16_t room_num = dst_item->room_num; - const SECTOR *const sector = - Room_GetSector(new_pos.x, new_pos.y, new_pos.z, &room_num); - const int32_t height = - Room_GetHeight(sector, new_pos.x, new_pos.y, new_pos.z); - const int32_t ceiling = - Room_GetCeiling(sector, new_pos.x, new_pos.y, new_pos.z); - - if (ABS(height - dst_item->pos.y) > STEP_L - || ABS(ceiling - dst_item->pos.y) < LARA_HEIGHT) { - return; - } - - dst_item->pos.x = new_pos.x; - dst_item->pos.y = new_pos.y; - dst_item->pos.z = new_pos.z; -} - int32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frames[], int32_t *rate) { const ANIM *const anim = Item_GetAnim(item); diff --git a/src/tr2/game/items.h b/src/tr2/game/items.h index cb24b329a..c82d6ec0c 100644 --- a/src/tr2/game/items.h +++ b/src/tr2/game/items.h @@ -8,8 +8,6 @@ void Item_Control(void); void Item_ClearKilled(void); void Item_ShiftCol(ITEM *item, COLL_INFO *coll); void Item_UpdateRoom(ITEM *item, int32_t height); -void Item_AlignPosition( - const XYZ_32 *vec, const ITEM *src_item, ITEM *dst_item); int32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frmptr[], int32_t *rate); bool Item_IsNearItem(const ITEM *item, const XYZ_32 *pos, int32_t distance); diff --git a/src/tr2/game/objects/general/detonator.c b/src/tr2/game/objects/general/detonator.c index ec84db708..e0b0120a3 100644 --- a/src/tr2/game/objects/general/detonator.c +++ b/src/tr2/game/objects/general/detonator.c @@ -140,7 +140,7 @@ static void M_Collision( } Inv_RemoveItem(O_KEY_OPTION_2); - Item_AlignPosition(&m_DetonatorPosition, item, lara_item); + Lara_AlignPosition(item, &m_DetonatorPosition); Item_SwitchToObjAnim(lara_item, LA_EXTRA_BREATH, 0, O_LARA_EXTRA); lara_item->current_anim_state = LA_EXTRA_BREATH; if (item->object_id == O_DETONATOR_2) { diff --git a/src/tr2/game/objects/general/keyhole.c b/src/tr2/game/objects/general/keyhole.c index c564cd3df..c9b44a88f 100644 --- a/src/tr2/game/objects/general/keyhole.c +++ b/src/tr2/game/objects/general/keyhole.c @@ -57,7 +57,7 @@ static void M_Consume( const GAME_OBJECT_ID key_obj_id) { Inv_RemoveItem(key_obj_id); - Item_AlignPosition(&m_KeyholePosition, keyhole_item, lara_item); + Lara_AlignPosition(keyhole_item, &m_KeyholePosition); lara_item->goal_anim_state = LS_USE_KEY; do { Lara_Animate(lara_item); diff --git a/src/tr2/game/objects/general/pickup.c b/src/tr2/game/objects/general/pickup.c index dab94658f..2f1853f3f 100644 --- a/src/tr2/game/objects/general/pickup.c +++ b/src/tr2/game/objects/general/pickup.c @@ -134,7 +134,7 @@ static void M_DoAboveWater(const int16_t item_num, ITEM *const lara_item) lara_item->goal_anim_state = LS_STOP; g_Lara.gun_status = LGS_HANDS_BUSY; } else { - Item_AlignPosition(&m_PickupPosition, item, lara_item); + Lara_AlignPosition(item, &m_PickupPosition); lara_item->goal_anim_state = LS_PICKUP; do { Lara_Animate(lara_item); diff --git a/src/tr2/game/objects/general/puzzle_hole.c b/src/tr2/game/objects/general/puzzle_hole.c index 45aa8fe52..f63db4c5c 100644 --- a/src/tr2/game/objects/general/puzzle_hole.c +++ b/src/tr2/game/objects/general/puzzle_hole.c @@ -58,7 +58,7 @@ static void M_Consume( const GAME_OBJECT_ID puzzle_obj_id) { Inv_RemoveItem(puzzle_obj_id); - Item_AlignPosition(&m_PuzzleHolePosition, puzzle_hole_item, lara_item); + Lara_AlignPosition(puzzle_hole_item, &m_PuzzleHolePosition); lara_item->goal_anim_state = LS_USE_PUZZLE; do { Lara_Animate(lara_item); diff --git a/src/tr2/game/objects/general/switch.c b/src/tr2/game/objects/general/switch.c index 2053d2a4e..6d3de1203 100644 --- a/src/tr2/game/objects/general/switch.c +++ b/src/tr2/game/objects/general/switch.c @@ -66,15 +66,15 @@ static void M_AlignLara(ITEM *const lara_item, ITEM *const switch_item) { switch (switch_item->object_id) { case O_SWITCH_TYPE_AIRLOCK: - Item_AlignPosition(&m_AirlockPosition, switch_item, lara_item); + Lara_AlignPosition(switch_item, &m_AirlockPosition); break; case O_SWITCH_TYPE_SMALL: - Item_AlignPosition(&g_SmallSwitchPosition, switch_item, lara_item); + Lara_AlignPosition(switch_item, &g_SmallSwitchPosition); break; case O_SWITCH_TYPE_BUTTON: - Item_AlignPosition(&g_PushSwitchPosition, switch_item, lara_item); + Lara_AlignPosition(switch_item, &g_PushSwitchPosition); break; } } diff --git a/src/tr2/game/objects/general/zipline.c b/src/tr2/game/objects/general/zipline.c index 447cfa4fa..0effd9bb6 100644 --- a/src/tr2/game/objects/general/zipline.c +++ b/src/tr2/game/objects/general/zipline.c @@ -156,7 +156,7 @@ static void M_Collision( return; } - Item_AlignPosition(&m_ZiplineHandlePosition, item, lara_item); + Lara_AlignPosition(item, &m_ZiplineHandlePosition); g_Lara.gun_status = LGS_HANDS_BUSY; lara_item->goal_anim_state = LS_ZIPLINE; From a859d668f915983aa1e8e5a49e7d5a48394bce5c Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Sun, 27 Apr 2025 18:59:38 +0100 Subject: [PATCH 43/52] lara: test alignment position height validity in TR1 This applies a fix introduced in TR2 to TR1 so that Lara doesn't become clamped under a steeply sloped ceiling if picking up an item there. Resolves #2879. --- docs/tr1/CHANGELOG.md | 1 + docs/tr1/README.md | 1 + src/libtrx/game/lara/common.c | 3 --- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index e885f60fe..97c2ae10f 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -25,6 +25,7 @@ - fixed the scale of the four keys in St. Francis' Folly (#2652) - fixed the panther at times not making a sound when it dies, and restored Skate Kid's death SFX (#2647) - fixed pushblocks being rotated when Lara grabs them, most noticeable if asymmetric textures have been used (#2776) +- fixed Lara becoming clamped if she picks up an item under a steeply sloped ceiling (#2879) - fixed a crash when 3D pickups are disabled and Lara crosses a trigger to look at a pickup item (#2711, regression from 4.8) - fixed trapezoid filter warping on faces close to the camera (#2629, regression from 4.9) - fixed Mac builds crashing upon start (regression from 4.9) diff --git a/docs/tr1/README.md b/docs/tr1/README.md index d128fb4b3..e49c78f4e 100644 --- a/docs/tr1/README.md +++ b/docs/tr1/README.md @@ -485,6 +485,7 @@ Not all options are turned on by default. Refer to `TR1X_ConfigTool.exe` for det - fixed being able to shoot the scion multiple times if save/load is used while it blows up - fixed the game crashing if a cinematic is triggered but the level contains no cinematic frames - fixed pushblocks being rotated when Lara grabs them, most noticeable if asymmetric textures have been used +- fixed Lara becoming clamped if she picks up an item under a steeply sloped ceiling #### Cheats - added a fly cheat diff --git a/src/libtrx/game/lara/common.c b/src/libtrx/game/lara/common.c index eded61e70..ad6136f5a 100644 --- a/src/libtrx/game/lara/common.c +++ b/src/libtrx/game/lara/common.c @@ -228,8 +228,6 @@ void Lara_AlignPosition(const ITEM *const item, const XYZ_32 *const vec) .z = item->pos.z + shift.z, }; -#if TR_VERSION == 2 - // TODO: check the significance of this in TR1 int16_t room_num = lara->room_num; const SECTOR *const sector = Room_GetSector(new_pos.x, new_pos.y, new_pos.z, &room_num); @@ -242,7 +240,6 @@ void Lara_AlignPosition(const ITEM *const item, const XYZ_32 *const vec) || ABS(ceiling - lara->pos.y) < LARA_HEIGHT) { return; } -#endif lara->pos = new_pos; } From 6d5bdd89a371c2ad3d242230f5e63d5e87aa2b00 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Mon, 28 Apr 2025 00:11:02 +0200 Subject: [PATCH 44/52] ui: improve modal sizing --- src/libtrx/game/ui/common.c | 22 +++++++++++++++++++ src/libtrx/game/ui/dialogs/photo_mode.c | 2 +- src/libtrx/game/ui/elements/modal.c | 2 +- src/libtrx/include/libtrx/game/ui/common.h | 8 +++---- src/tr1/game/ui/common.c | 24 --------------------- src/tr1/meson.build | 1 - src/tr2/game/ui/common.c | 25 ---------------------- src/tr2/meson.build | 1 - 8 files changed, 28 insertions(+), 57 deletions(-) delete mode 100644 src/tr1/game/ui/common.c delete mode 100644 src/tr2/game/ui/common.c diff --git a/src/libtrx/game/ui/common.c b/src/libtrx/game/ui/common.c index 2ffa2c32b..159eb7190 100644 --- a/src/libtrx/game/ui/common.c +++ b/src/libtrx/game/ui/common.c @@ -4,8 +4,10 @@ #include "debug.h" #include "game/console/common.h" #include "game/game_string.h" +#include "game/scaler.h" #include "game/ui/elements/anchor.h" #include "game/ui/events.h" +#include "game/viewport.h" #include "memory.h" #include @@ -201,3 +203,23 @@ void UI_HandleTextEdit(const char *const text) UI_FireEvent((EVENT) { .name = "text_edit", .sender = nullptr, .data = (void *)text }); } + +int32_t UI_GetCanvasWidth(void) +{ + return Scaler_CalcInverse(Viewport_GetWidth(), SCALER_TARGET_GENERIC); +} + +int32_t UI_GetCanvasHeight(void) +{ + return Scaler_CalcInverse(Viewport_GetHeight(), SCALER_TARGET_GENERIC); +} + +float UI_ScaleX(const float x) +{ + return Scaler_Calc(x, SCALER_TARGET_GENERIC); +} + +float UI_ScaleY(const float y) +{ + return Scaler_Calc(y, SCALER_TARGET_GENERIC); +} diff --git a/src/libtrx/game/ui/dialogs/photo_mode.c b/src/libtrx/game/ui/dialogs/photo_mode.c index 512f4b3e1..903fe6a11 100644 --- a/src/libtrx/game/ui/dialogs/photo_mode.c +++ b/src/libtrx/game/ui/dialogs/photo_mode.c @@ -21,7 +21,7 @@ void UI_PhotoMode(void) char tmp[50]; UI_BeginModal(0.0f, 0.0f); - UI_BeginPad(8.0f, 10.0f); + UI_BeginPad(8.0f, 8.0f); UI_BeginFrame(UI_FRAME_DIALOG_BACKGROUND); UI_BeginPad(8.0, 6.0); diff --git a/src/libtrx/game/ui/elements/modal.c b/src/libtrx/game/ui/elements/modal.c index 9aec8632e..f74030b87 100644 --- a/src/libtrx/game/ui/elements/modal.c +++ b/src/libtrx/game/ui/elements/modal.c @@ -15,7 +15,7 @@ static const UI_WIDGET_OPS m_Ops = { static void M_Measure(UI_NODE *const node) { node->measure_w = UI_GetCanvasWidth(); - node->measure_h = UI_GetCanvasHeight() - TEXT_HEIGHT_FIXED; + node->measure_h = UI_GetCanvasHeight(); } void UI_BeginModal(const float x, const float y) diff --git a/src/libtrx/include/libtrx/game/ui/common.h b/src/libtrx/include/libtrx/game/ui/common.h index 6400264f1..015dfc5f3 100644 --- a/src/libtrx/include/libtrx/game/ui/common.h +++ b/src/libtrx/include/libtrx/game/ui/common.h @@ -50,10 +50,10 @@ typedef struct UI_NODE { // Dimensions in virtual pixels of the screen area // (640x480 for any 4:3 resolution on 1.00 text scaling) -extern int32_t UI_GetCanvasWidth(void); -extern int32_t UI_GetCanvasHeight(void); -extern float UI_ScaleX(float x); -extern float UI_ScaleY(float y); +int32_t UI_GetCanvasWidth(void); +int32_t UI_GetCanvasHeight(void); +float UI_ScaleX(float x); +float UI_ScaleY(float y); // Public API for scene management void UI_BeginScene(void); diff --git a/src/tr1/game/ui/common.c b/src/tr1/game/ui/common.c deleted file mode 100644 index b3cdf81f9..000000000 --- a/src/tr1/game/ui/common.c +++ /dev/null @@ -1,24 +0,0 @@ -#include "game/screen.h" - -#include -#include - -int32_t UI_GetCanvasWidth(void) -{ - return Screen_GetResHeightDownscaled(RSR_GENERIC) * 16 / 9; -} - -int32_t UI_GetCanvasHeight(void) -{ - return Screen_GetResHeightDownscaled(RSR_GENERIC); -} - -float UI_ScaleX(const float x) -{ - return Screen_GetRenderScale(x * 0x10000, RSR_GENERIC) / (float)0x10000; -} - -float UI_ScaleY(const float y) -{ - return Screen_GetRenderScale(y * 0x10000, RSR_GENERIC) / (float)0x10000; -} diff --git a/src/tr1/meson.build b/src/tr1/meson.build index 71f6c3254..a047f9ce8 100644 --- a/src/tr1/meson.build +++ b/src/tr1/meson.build @@ -260,7 +260,6 @@ sources = [ 'game/spawn.c', 'game/stats/common.c', 'game/text.c', - 'game/ui/common.c', 'game/ui/dialogs/stats.c', 'game/viewport.c', 'global/enum_map.c', diff --git a/src/tr2/game/ui/common.c b/src/tr2/game/ui/common.c deleted file mode 100644 index f4b10ec67..000000000 --- a/src/tr2/game/ui/common.c +++ /dev/null @@ -1,25 +0,0 @@ -#include "global/vars.h" - -#include -#include -#include - -int32_t UI_GetCanvasWidth(void) -{ - return Scaler_CalcInverse(g_PhdWinWidth, SCALER_TARGET_GENERIC); -} - -int32_t UI_GetCanvasHeight(void) -{ - return Scaler_CalcInverse(g_PhdWinHeight, SCALER_TARGET_GENERIC); -} - -float UI_ScaleX(const float x) -{ - return Scaler_Calc(x, SCALER_TARGET_GENERIC); -} - -float UI_ScaleY(const float y) -{ - return Scaler_Calc(y, SCALER_TARGET_GENERIC); -} diff --git a/src/tr2/meson.build b/src/tr2/meson.build index 80c5e9c77..b3d748095 100644 --- a/src/tr2/meson.build +++ b/src/tr2/meson.build @@ -266,7 +266,6 @@ sources = [ 'game/spawn.c', 'game/stats.c', 'game/text.c', - 'game/ui/common.c', 'game/ui/dialogs/graphic_settings.c', 'game/ui/dialogs/stats.c', 'game/viewport.c', From 5e3fb42a2534488f8f4a135051e58b853715d3e0 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Mon, 28 Apr 2025 11:21:05 +0200 Subject: [PATCH 45/52] docs: improve release process documentation --- docs/CONTRIBUTING.md | 63 +++++++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 7ea40e63f..efcdc6a13 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -266,30 +266,63 @@ request number, but it's important to carefully review the body field, as it often includes unwanted content. -### Branching model - -We have two branches: `develop` and `stable`. `develop` is where all changes -about to be published in the next release land. `stable` is the latest release. -We avoid creating merge commits between these two – they should always point to -the same HEAD when applicable. This means that any hotfixes that need to be -released ahead of unpublished work in `develop` are merged directly to -`stable`, and `develop` needs to be then rebased on top of the now-patched -`stable`. - - ### Tooling Internal tools are typically coded in a reasonably recent version of Python, while avoiding the use of bash, shell, and similar languages. +### Branching model + +We have two branches: `develop` and `stable`. `develop` is where all changes +about to be published in the next release land. `stable` is the latest release. + + ### Releasing a new version -New version releases happen automatically whenever a new tag is pushed to the -`stable` branch with the help of GitHub actions. In general this is accompanied -with a special commit `docs: release X.Y.Z` that also adjusts the changelog. -See git history for details. +New version releases are published automatically whenever a new tag is pushed +to the `stable` branch with the help of GitHub actions. +The general workflow is this: +```console +TR_VERSION=... +RELEASE_VERSION=... + +# Switch to the stable branch. +git checkout stable + +# Merge `develop` into it. +git merge develop + +# Create a special commit `docs: release X.Y.Z` marking the release in the +# relevant changelog file. Then tag it with `tr1-X.Y.Z` or `tr2-X.Y.Z`. +# You can do that by hand, or run the command below: +tools/release commit ${TR_VERSION} ${RELEASE_VERSION} + +# Review the changelog content. + +# Switch back to develop. +git checkout develop + +# Merge stable using fast-forward. +git merge --ff stable + +# Review both branches and changes. If everything is okay, push to GitHub. +# You can do this by hand: git push origin develop stable tr1-X.Y.Z, or: +# tools/release push ${TR_VERSION} ${RELEASE_VERSION} +``` + +### Hotfixes + +Hotfix releases are a bit different as we try to not include non-bugfix changes +in them. Here instead of merging `develop` to `stable` we cherry-pick relevant +changes, resolving conflicts along the way. + +### Versioning + +We increase the major version for significant releases based on judgment, +typically defaulting to increasing the minor version. Hotfixes increase the +patch version. ## Glossary From 7ce52cad2f027a4a4a46ae0a5aadccd120fcc175 Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Mon, 28 Apr 2025 19:55:43 +0100 Subject: [PATCH 46/52] tr2/objects/switch: align Lara after airlock test This ensures Lara's angle is only aligned after all other collision/state tests for switches/airlocks. Resolves #2215. --- docs/tr2/CHANGELOG.md | 1 + docs/tr2/README.md | 1 + src/tr2/game/objects/general/switch.c | 3 +-- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index a7e9c6255..303ea6db3 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -4,6 +4,7 @@ - changed the sound dialog appearance (repositioned, added text labels and arrows) - fixed Lara being killed if she enters the void in a level that uses the `disable_floor` sequence in the game flow (#2874, regression from 0.10) - fixed flame emitter 23 in room 6 not being deactivated when the lever in room 1 is used (#2851) +- fixed Lara snapping to face forwards if she has a slight angle and action is pressed after using an airlock door (#2215) ## [1.0.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...tr2-1.0.2) - 2025-04-26 - changed The Golden Mask strings to default to the OG strings file for the main tables (#2847) diff --git a/docs/tr2/README.md b/docs/tr2/README.md index 4916baac5..fd4246446 100644 --- a/docs/tr2/README.md +++ b/docs/tr2/README.md @@ -258,6 +258,7 @@ However, you can easily download them manually from these urls: - fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level - fixed destroyed gondolas appearing embedded in the ground after loading a save - fixed Lara voiding if she stops on a tile with a closing door, and the door isn't on a portal +- fixed Lara snapping to face forwards if she has a slight angle and action is pressed after using an airlock door - improved the animation of Lara's braid #### Cheats diff --git a/src/tr2/game/objects/general/switch.c b/src/tr2/game/objects/general/switch.c index 6d3de1203..382425e6f 100644 --- a/src/tr2/game/objects/general/switch.c +++ b/src/tr2/game/objects/general/switch.c @@ -64,6 +64,7 @@ static const OBJECT_BOUNDS *M_BoundsUW(void) static void M_AlignLara(ITEM *const lara_item, ITEM *const switch_item) { + lara_item->rot.y = switch_item->rot.y; switch (switch_item->object_id) { case O_SWITCH_TYPE_AIRLOCK: Lara_AlignPosition(switch_item, &m_AirlockPosition); @@ -168,8 +169,6 @@ static void M_Collision( return; } - lara_item->rot.y = item->rot.y; - if (item->object_id == O_SWITCH_TYPE_AIRLOCK && item->current_anim_state == SWITCH_STATE_ON) { return; From c91ef2ce554d7d1a243cb7d18b8de54c8bba8974 Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:27:56 +0100 Subject: [PATCH 47/52] tr2/data: add explosion sprites to HSH Resolves #1569. --- data/tr2/ship/cfg/TR2X_gameflow.json5 | 1 + data/tr2/ship/data/injections/explosion.bin | Bin 0 -> 57264 bytes docs/tr2/CHANGELOG.md | 1 + 3 files changed, 2 insertions(+) create mode 100644 data/tr2/ship/data/injections/explosion.bin diff --git a/data/tr2/ship/cfg/TR2X_gameflow.json5 b/data/tr2/ship/cfg/TR2X_gameflow.json5 index 38a4c9665..2649e668d 100644 --- a/data/tr2/ship/cfg/TR2X_gameflow.json5 +++ b/data/tr2/ship/cfg/TR2X_gameflow.json5 @@ -398,6 +398,7 @@ {"type": "total_stats", "background_path": "data/images/end.webp"}, ], "injections": [ + "data/injections/explosion.bin", "data/injections/house_itemrots.bin", ], }, diff --git a/data/tr2/ship/data/injections/explosion.bin b/data/tr2/ship/data/injections/explosion.bin new file mode 100644 index 0000000000000000000000000000000000000000..9972c25aa717612c3d83240e49749d9ce6b1f42c GIT binary patch literal 57264 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 literal 0 HcmV?d00001 diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 303ea6db3..62e808653 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...develop) - ××××-××-×× - added aliases to CLI options (`-gold` becomes `-g/--gold`) - added a `--help` CLI option (may not output anything on Windows machines – OS bug) +- added explosion sprites to Home Sweet Home (#1569) - changed the sound dialog appearance (repositioned, added text labels and arrows) - fixed Lara being killed if she enters the void in a level that uses the `disable_floor` sequence in the game flow (#2874, regression from 0.10) - fixed flame emitter 23 in room 6 not being deactivated when the lever in room 1 is used (#2851) From 9daa77a7ca73bfc67f4c29a11697afe0219b82d1 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Tue, 29 Apr 2025 09:21:49 +0200 Subject: [PATCH 48/52] audio/stream: improve seeking resilience Potentially impacts #1762. --- src/libtrx/engine/audio_stream.c | 72 +++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/src/libtrx/engine/audio_stream.c b/src/libtrx/engine/audio_stream.c index bca11ca75..dfcf05d0d 100644 --- a/src/libtrx/engine/audio_stream.c +++ b/src/libtrx/engine/audio_stream.c @@ -84,17 +84,32 @@ static void M_SeekToStart(AUDIO_STREAM_SOUND *stream) ASSERT(stream != nullptr); stream->timestamp = stream->start_at; + int32_t error_code; if (stream->start_at <= 0.0) { // reset to start of file avio_seek(stream->av.format_ctx->pb, 0, SEEK_SET); - avformat_seek_file( + error_code = avformat_seek_file( stream->av.format_ctx, -1, 0, 0, 0, AVSEEK_FLAG_FRAME); } else { // seek to specific timestamp - const double time_base_sec = av_q2d(stream->av.stream->time_base); - av_seek_frame( - stream->av.format_ctx, 0, stream->start_at / time_base_sec, - AVSEEK_FLAG_ANY); + AVFormatContext *const fmt = stream->av.format_ctx; + if (fmt->pb != nullptr && (fmt->pb->seekable & AVIO_SEEKABLE_NORMAL)) { + const int64_t ts = (int64_t)(stream->start_at * AV_TIME_BASE); + error_code = avformat_seek_file( + fmt, stream->av.stream->index, INT64_MIN, ts, INT64_MAX, + AVSEEK_FLAG_BACKWARD); + } else { + // fallback to stream-based seek + const double time_base_sec = av_q2d(stream->av.stream->time_base); + error_code = av_seek_frame( + fmt, stream->av.stream->index, + (int64_t)(stream->start_at / time_base_sec), AVSEEK_FLAG_ANY); + } + } + if (error_code < 0) { + LOG_ERROR( + "seek failed for timestamp %f: %s", stream->timestamp, + av_err2str(error_code)); } } @@ -680,26 +695,51 @@ double Audio_Stream_GetDuration(int32_t sound_id) return duration; } -bool Audio_Stream_SeekTimestamp(int32_t sound_id, double timestamp) +bool Audio_Stream_SeekTimestamp(const int32_t sound_id, const double timestamp) { if (!g_AudioDeviceID || sound_id < 0 || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) { return false; } - if (m_Streams[sound_id].is_playing) { - SDL_LockAudioDevice(g_AudioDeviceID); - AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id]; - const double time_base_sec = av_q2d(stream->av.stream->time_base); - av_seek_frame( - stream->av.format_ctx, 0, timestamp / time_base_sec, - AVSEEK_FLAG_ANY); - avcodec_flush_buffers(stream->av.codec_ctx); + AUDIO_STREAM_SOUND *const stream = &m_Streams[sound_id]; + stream->start_at = timestamp; + if (!stream->is_used) { + return false; + } + ASSERT(stream->av.format_ctx != nullptr); + ASSERT(stream->av.codec_ctx != nullptr); + ASSERT(stream->av.stream != nullptr); + + SDL_LockAudioDevice(g_AudioDeviceID); + + const double time_base_sec = av_q2d(stream->av.stream->time_base); + if (time_base_sec <= 0.0) { + LOG_ERROR( + "Audio_Stream_SeekTimestamp: invalid time_base %f", time_base_sec); SDL_UnlockAudioDevice(g_AudioDeviceID); - return true; + return false; } - return false; + const int32_t stream_index = stream->av.stream->index; + const int64_t seek_target = (int64_t)(timestamp / time_base_sec); + const int32_t error_code = av_seek_frame( + stream->av.format_ctx, stream_index, seek_target, AVSEEK_FLAG_ANY); + if (error_code < 0) { + LOG_ERROR( + "seek failed for timestamp %f: %s", timestamp, + av_err2str(error_code)); + } + + avcodec_flush_buffers(stream->av.codec_ctx); + if (stream->sdl.stream) { + SDL_AudioStreamFlush(stream->sdl.stream); + } + + stream->timestamp = timestamp; + + SDL_UnlockAudioDevice(g_AudioDeviceID); + return true; } bool Audio_Stream_SetStartTimestamp(int32_t sound_id, double timestamp) From 8db873b705914ecc19c5b192b97d768f47785f0b Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Tue, 29 Apr 2025 09:28:16 +0200 Subject: [PATCH 49/52] audio/stream: fix crashes if the file does not exist Resolves #2887. --- docs/tr1/CHANGELOG.md | 1 + src/libtrx/engine/audio_stream.c | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index 97c2ae10f..33510fe70 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -42,6 +42,7 @@ - fixed the `/pos` console command reporting the base room number when Lara is actually in a flipped room (#2487, regression from 3.0) - fixed clicks in audio sounds (#2846, regression from 2.0) - fixed Lara being killed if she enters the void in a level that uses the `disable_floor` sequence in the game flow (#2874, regression from 4.9) +- fixed game crashing if the music folder was not present (#2887, regression from 4.9) - improved bubble appearance (#2672) - improved rendering performance - improved pause exit dialog - it can now be canceled with escape diff --git a/src/libtrx/engine/audio_stream.c b/src/libtrx/engine/audio_stream.c index dfcf05d0d..ba80da5f7 100644 --- a/src/libtrx/engine/audio_stream.c +++ b/src/libtrx/engine/audio_stream.c @@ -272,6 +272,10 @@ static bool M_InitialiseFromPath(int32_t sound_id, const char *file_path) int32_t error_code; char *full_path = File_GetFullPath(file_path); + if (full_path == nullptr) { + error_code = AVERROR(ENOENT); + goto cleanup; + } AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id]; From ff4c1dc2ece2b15c1363ee018041e0aa4dedf734 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Tue, 29 Apr 2025 13:08:32 +0200 Subject: [PATCH 50/52] console/cmd: make /set error output more helpful --- data/tr1/ship/cfg/TR1X_strings.json5 | 4 ++ data/tr2/ship/cfg/TR2X_strings.json5 | 4 ++ docs/tr1/CHANGELOG.md | 1 + docs/tr2/CHANGELOG.md | 1 + src/libtrx/enum_map.c | 22 ++++++ src/libtrx/game/console/cmd/config.c | 68 +++++++++++++++++-- src/libtrx/include/libtrx/enum_map.h | 9 ++- .../include/libtrx/game/game_string.def | 4 ++ 8 files changed, 107 insertions(+), 6 deletions(-) diff --git a/data/tr1/ship/cfg/TR1X_strings.json5 b/data/tr1/ship/cfg/TR1X_strings.json5 index fe26d2eaa..b49fdf18d 100644 --- a/data/tr1/ship/cfg/TR1X_strings.json5 +++ b/data/tr1/ship/cfg/TR1X_strings.json5 @@ -428,7 +428,11 @@ "OSD_AMBIGUOUS_INPUT_2": "Ambiguous input: %s and %s", "OSD_AMBIGUOUS_INPUT_3": "Ambiguous input: %s, %s, ...", "OSD_COMMAND_BAD_INVOCATION": "Invalid invocation: %s", + "OSD_COMMAND_BOOL": "on, off", + "OSD_COMMAND_DECIMAL": "[decimal]", + "OSD_COMMAND_INTEGER": "[integer]", "OSD_COMMAND_UNAVAILABLE": "This command is not currently available", + "OSD_COMMAND_VALID_VALUES": "Valid values: %s", "OSD_COMPLETE_LEVEL": "Level complete!", "OSD_CONFIG_OPTION_GET": "%s is currently set to %s", "OSD_CONFIG_OPTION_SET": "%s changed to %s", diff --git a/data/tr2/ship/cfg/TR2X_strings.json5 b/data/tr2/ship/cfg/TR2X_strings.json5 index e2d2e8c0c..5d08c0f49 100644 --- a/data/tr2/ship/cfg/TR2X_strings.json5 +++ b/data/tr2/ship/cfg/TR2X_strings.json5 @@ -547,7 +547,11 @@ "OSD_BILINEAR_FILTER_OFF": "Bilinear filter: off", "OSD_BILINEAR_FILTER_ON": "Bilinear filter: on", "OSD_COMMAND_BAD_INVOCATION": "Invalid invocation: %s", + "OSD_COMMAND_BOOL": "on, off", + "OSD_COMMAND_DECIMAL": "[decimal]", + "OSD_COMMAND_INTEGER": "[integer]", "OSD_COMMAND_UNAVAILABLE": "This command is not currently available", + "OSD_COMMAND_VALID_VALUES": "Valid values: %s", "OSD_COMPLETE_LEVEL": "Level complete!", "OSD_CONFIG_OPTION_GET": "%s is currently set to %s", "OSD_CONFIG_OPTION_SET": "%s changed to %s", diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index 33510fe70..1553dcf26 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -46,6 +46,7 @@ - improved bubble appearance (#2672) - improved rendering performance - improved pause exit dialog - it can now be canceled with escape +- improved the `/set` console command to display available options if given an unknown argument - removed the pretty pixels options (it's now always enabled, #2258) ## [4.9](https://github.com/LostArtefacts/TRX/compare/tr1-4.8.3...tr1-4.9) - 2025-03-31 diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 62e808653..af7855da8 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -6,6 +6,7 @@ - fixed Lara being killed if she enters the void in a level that uses the `disable_floor` sequence in the game flow (#2874, regression from 0.10) - fixed flame emitter 23 in room 6 not being deactivated when the lever in room 1 is used (#2851) - fixed Lara snapping to face forwards if she has a slight angle and action is pressed after using an airlock door (#2215) +- improved the `/set` console command to display available options if given an unknown argument ## [1.0.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...tr2-1.0.2) - 2025-04-26 - changed The Golden Mask strings to default to the OG strings file for the main tables (#2847) diff --git a/src/libtrx/enum_map.c b/src/libtrx/enum_map.c index b140bfc30..88cf557f3 100644 --- a/src/libtrx/enum_map.c +++ b/src/libtrx/enum_map.c @@ -121,3 +121,25 @@ void EnumMap_Shutdown(void) } } } + +VECTOR *EnumMap_ListValues(const char *const enum_name) +{ + if (enum_name == nullptr) { + return nullptr; + } + + // Compare the prefix to find the matching enum values. + const size_t prefix_len = strlen(enum_name) + 1; + + VECTOR *const results = Vector_Create(sizeof(char *)); + M_INVERSE_ENTRY *entry; + M_INVERSE_ENTRY *tmp; + HASH_ITER(hh, m_InverseMap, entry, tmp) + { + if (strncmp(entry->key, enum_name, prefix_len - 1) == 0 + && entry->key[prefix_len - 1] == '|') { + Vector_Add(results, &entry->str_value); + } + } + return results; +} diff --git a/src/libtrx/game/console/cmd/config.c b/src/libtrx/game/console/cmd/config.c index 4eccfe895..bd2f31bca 100644 --- a/src/libtrx/game/console/cmd/config.c +++ b/src/libtrx/game/console/cmd/config.c @@ -14,6 +14,7 @@ static const char *M_Resolve(const char *option_name); static bool M_SameKey(const char *key1, const char *key2); +static char *M_GetAvailableOptions(const CONFIG_OPTION *option); static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx); @@ -76,6 +77,56 @@ cleanup: return result; } +// Return a comma-delimited list of valid values for the option. +// Caller must free the result via Memory_FreePointer. +static char *M_GetAvailableOptions(const CONFIG_OPTION *const option) +{ + if (option == nullptr) { + return nullptr; + } + + switch (option->type) { + case COT_BOOL: + return Memory_DupStr(GS(OSD_COMMAND_BOOL)); + + case COT_INT32: + return Memory_DupStr(GS(OSD_COMMAND_INTEGER)); + + case COT_DOUBLE: + case COT_FLOAT: + return Memory_DupStr(GS(OSD_COMMAND_DECIMAL)); + + case COT_ENUM: { + const char *enum_name = (const char *)option->param; + VECTOR *const values = EnumMap_ListValues(enum_name); + if (values == nullptr) { + return nullptr; + } + // Join vector items into a comma-separated string + size_t total_len = 1; + const char *const sep = ", "; + for (int32_t i = 0; i < values->count; i++) { + const char *const s = *(char **)Vector_Get(values, i); + total_len += strlen(s) + (i + 1 < values->count ? strlen(sep) : 0); + } + char *const result = Memory_Alloc(total_len); + char *ptr = result; + for (int32_t i = 0; i < values->count; i++) { + const char *const s = *(char **)Vector_Get(values, i); + strcat(ptr, s); + if (i + 1 < values->count) { + strcat(ptr, sep); + } + } + Vector_Free(values); + return result; + } + + default: + return nullptr; + } +} + char *Console_Cmd_Config_NormalizeKey(const char *key) { // TODO: Once we support arbitrary glyphs, this conversion should @@ -261,18 +312,16 @@ COMMAND_RESULT Console_Cmd_Config_Helper( char *normalized_name = Console_Cmd_Config_NormalizeKey(option->name); - COMMAND_RESULT result = CR_BAD_INVOCATION; if (new_value == nullptr || String_IsEmpty(new_value)) { char cur_value[128]; if (Console_Cmd_Config_GetCurrentValue(option, cur_value, 128)) { Console_Log(GS(OSD_CONFIG_OPTION_GET), normalized_name, cur_value); - result = CR_SUCCESS; - } else { - result = CR_FAILURE; + return CR_SUCCESS; } - return result; + return CR_FAILURE; } + COMMAND_RESULT result; if (Console_Cmd_Config_SetCurrentValue(option, new_value)) { Config_Write(); @@ -280,6 +329,15 @@ COMMAND_RESULT Console_Cmd_Config_Helper( ASSERT(Console_Cmd_Config_GetCurrentValue(option, final_value, 128)); Console_Log(GS(OSD_CONFIG_OPTION_SET), normalized_name, final_value); result = CR_SUCCESS; + } else { + // Report bad invocation on the provided new value + Console_Log(GS(OSD_COMMAND_BAD_INVOCATION), new_value); + char *available_options = M_GetAvailableOptions(option); + if (available_options != nullptr) { + Console_Log(GS(OSD_COMMAND_VALID_VALUES), available_options); + Memory_FreePointer(&available_options); + } + result = CR_FAILURE; } cleanup: diff --git a/src/libtrx/include/libtrx/enum_map.h b/src/libtrx/include/libtrx/enum_map.h index d0fecff85..12382e49f 100644 --- a/src/libtrx/include/libtrx/enum_map.h +++ b/src/libtrx/include/libtrx/enum_map.h @@ -1,4 +1,4 @@ -#include +#include "vector.h" #define ENUM_MAP_DEFINE(enum_name, enum_value, str_value) \ EnumMap_Define(ENUM_MAP_NAME(enum_name), enum_value, str_value); @@ -18,6 +18,13 @@ extern void EnumMap_Init(void); void EnumMap_Shutdown(void); +// Returns a vector of valid string values for the given enum_name. +// +// The returned vector must be freed via Vector_Free(). The string pointers +// within the vector are owned by the enum map and should not be freed by the +// caller. Returns nullptr if the enum_name is not valid. +VECTOR *EnumMap_ListValues(const char *enum_name); + void EnumMap_Define( const char *enum_name, int32_t enum_value, const char *str_value); int32_t EnumMap_Get( diff --git a/src/libtrx/include/libtrx/game/game_string.def b/src/libtrx/include/libtrx/game/game_string.def index b1cadca94..7ce161412 100644 --- a/src/libtrx/include/libtrx/game/game_string.def +++ b/src/libtrx/include/libtrx/game/game_string.def @@ -34,6 +34,10 @@ GS_DEFINE(OSD_PLAY_MUSIC_TRACK, "Playing music track %d") GS_DEFINE(OSD_INVALID_MUSIC_TRACK, "Invalid music track") GS_DEFINE(OSD_UNKNOWN_COMMAND, "Unknown command: %s") GS_DEFINE(OSD_COMMAND_BAD_INVOCATION, "Invalid invocation: %s") +GS_DEFINE(OSD_COMMAND_VALID_VALUES, "Valid values: %s") +GS_DEFINE(OSD_COMMAND_BOOL, "on, off") +GS_DEFINE(OSD_COMMAND_INTEGER, "[integer]") +GS_DEFINE(OSD_COMMAND_DECIMAL, "[decimal]") GS_DEFINE(OSD_COMMAND_UNAVAILABLE, "This command is not currently available") GS_DEFINE(OSD_INVALID_ROOM, "Invalid room: %d. Valid rooms are 0-%d") GS_DEFINE(OSD_POS_SET_POS, "Teleported to position: %.3f %.3f %.3f") From 70dd6ff0b368342d9f4aef2b745c0ce9f38476ff Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Tue, 29 Apr 2025 19:36:05 +0200 Subject: [PATCH 51/52] gf: fix invalid events handling --- docs/tr1/CHANGELOG.md | 1 + docs/tr2/CHANGELOG.md | 1 + src/libtrx/game/game_flow/reader.c | 9 ++++----- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index 1553dcf26..596504d82 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -43,6 +43,7 @@ - fixed clicks in audio sounds (#2846, regression from 2.0) - fixed Lara being killed if she enters the void in a level that uses the `disable_floor` sequence in the game flow (#2874, regression from 4.9) - fixed game crashing if the music folder was not present (#2887, regression from 4.9) +- fixed game crashing on unknown sequencer events - improved bubble appearance (#2672) - improved rendering performance - improved pause exit dialog - it can now be canceled with escape diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index af7855da8..7e24a8241 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -6,6 +6,7 @@ - fixed Lara being killed if she enters the void in a level that uses the `disable_floor` sequence in the game flow (#2874, regression from 0.10) - fixed flame emitter 23 in room 6 not being deactivated when the lever in room 1 is used (#2851) - fixed Lara snapping to face forwards if she has a slight angle and action is pressed after using an airlock door (#2215) +- fixed the game crashing on unknown sequencer events - improved the `/set` console command to display available options if given an unknown argument ## [1.0.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...tr2-1.0.2) - 2025-04-26 diff --git a/src/libtrx/game/game_flow/reader.c b/src/libtrx/game/game_flow/reader.c index 77ff36e2e..507e13e64 100644 --- a/src/libtrx/game/game_flow/reader.c +++ b/src/libtrx/game/game_flow/reader.c @@ -281,6 +281,10 @@ static size_t M_LoadSequenceEvent( const char *const type_str = JSON_ObjectGetString(event_obj, "type", ""); const GF_SEQUENCE_EVENT_TYPE type = ENUM_MAP_GET(GF_SEQUENCE_EVENT_TYPE, type_str, -1); + if (type == (GF_SEQUENCE_EVENT_TYPE)-1) { + Shell_ExitSystemFmt( + "Unknown game flow sequence event type: '%s'", type_str); + } const M_SEQUENCE_EVENT_HANDLER *handler = M_GetSequenceEventHandlers(); while (handler->event_type != (GF_SEQUENCE_EVENT_TYPE)-1 @@ -288,11 +292,6 @@ static size_t M_LoadSequenceEvent( handler++; } - if (handler->event_type != type) { - Shell_ExitSystemFmt( - "Unknown game flow sequence event type: '%s'", type); - } - int32_t extra_data_size = 0; if (handler->handler_func != nullptr) { extra_data_size = handler->handler_func( From 659fdbc79c44a954a4535d7ec527af936756fe14 Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:54:24 +0100 Subject: [PATCH 52/52] tools/installer: always allow TR2 music download Rather than second guess users' OG music setup, the installer will now always offer the option to download the required music files. Resolves #2891. --- docs/tr2/CHANGELOG.md | 1 + .../TR2X_Installer/Installers/GenericInstallSource.cs | 9 ++------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 7e24a8241..d6b197278 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -3,6 +3,7 @@ - added a `--help` CLI option (may not output anything on Windows machines – OS bug) - added explosion sprites to Home Sweet Home (#1569) - changed the sound dialog appearance (repositioned, added text labels and arrows) +- changed the installer to always allow downloading music files (#2891) - fixed Lara being killed if she enters the void in a level that uses the `disable_floor` sequence in the game flow (#2874, regression from 0.10) - fixed flame emitter 23 in room 6 not being deactivated when the lever in room 1 is used (#2851) - fixed Lara snapping to face forwards if she has a slight angle and action is pressed after using an airlock door (#2215) diff --git a/tools/installer/TR2X_Installer/Installers/GenericInstallSource.cs b/tools/installer/TR2X_Installer/Installers/GenericInstallSource.cs index f98813d91..b7ed06ad7 100644 --- a/tools/installer/TR2X_Installer/Installers/GenericInstallSource.cs +++ b/tools/installer/TR2X_Installer/Installers/GenericInstallSource.cs @@ -15,15 +15,10 @@ public abstract class GenericInstallSource : BaseInstallSource }; public override bool IsDownloadingMusicNeeded(string sourceDirectory) - { - return !Directory.Exists(Path.Combine(sourceDirectory, "audio")) - && !Directory.Exists(Path.Combine(sourceDirectory, "music")); - } + => true; public override bool IsDownloadingExpansionNeeded(string sourceDirectory) - { - return true; - } + => true; public override async Task CopyOriginalGameFiles( string sourceDirectory,