From 10b9bcc7809cb4a64846cbbf21736973caca41cb Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Wed, 23 Apr 2025 23:35:19 +0200 Subject: [PATCH 01/44] 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 02/44] 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 03/44] 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 04/44] 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 05/44] 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 06/44] 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 07/44] 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 08/44] 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 09/44] 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 10/44] 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 11/44] 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 12/44] 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 13/44] 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 14/44] 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 15/44] 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 16/44] 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 17/44] 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 18/44] 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 19/44] 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 20/44] 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 21/44] 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 22/44] 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 23/44] 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 24/44] 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 25/44] 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 26/44] 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 27/44] 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 28/44] 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 29/44] 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 30/44] 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 31/44] 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 32/44] 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 33/44] 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 34/44] 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 35/44] 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 36/44] 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 37/44] 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 38/44] 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 39/44] 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 40/44] 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 41/44] 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 42/44] 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 43/44] 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 44/44] 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,