tr1/gameflow: add support for item descriptions

This adds the ability to define descriptions for key, puzzle and
pickups items in the gameflow. Players can examine items that have a
description in the inventory. Indicators are shown to make this clear,
the idea being builders may want to use this feature to offer hints.

Description text is automatically wrapped to fit the screen, and very
long text will be paginated. New line and feed escape sequences are
supported in the text for manual layout.

This also removes the necessesity of having an empty strings object in
levels that don't have any key items.

Additionally, the update_gameflow tool has been adjusted to support the
use of curly braces inside strings (and indeed nested objects).

Resolves #1821.
This commit is contained in:
lahm86 2024-11-07 15:38:26 +00:00
parent d8f2a094f5
commit 33e4e9be1d
33 changed files with 641 additions and 49 deletions

View file

@ -47,7 +47,6 @@
{"type": "level_stats", "level_id": 0},
{"type": "exit_to_title"},
],
"strings": {},
},
// Level 1
@ -70,7 +69,6 @@
{"type": "level_stats", "level_id": 1},
{"type": "exit_to_level", "level_id": 2},
],
"strings": {},
},
// Level 2
@ -143,7 +141,6 @@
{"type": "stop_game"},
{"type": "exit_to_cine", "level_id": 16},
],
"strings": {},
},
// Level 5
@ -418,7 +415,6 @@
{"type": "play_fmv", "fmv_path": "fmv/prison.avi"},
{"type": "exit_to_cine", "level_id": 19},
],
"strings": {},
"unobtainable_pickups": 3,
},
@ -448,7 +444,6 @@
{"type": "total_stats", "picture_path": "data/images/install.webp"},
{"type": "exit_to_title"},
],
"strings": {},
},
// Level 16
@ -474,7 +469,6 @@
{"type": "level_stats", "level_id": 4},
{"type": "exit_to_level", "level_id": 5},
],
"strings": {},
},
// Level 17
@ -502,7 +496,6 @@
{"type": "level_stats", "level_id": 9},
{"type": "exit_to_level", "level_id": 10},
],
"strings": {},
},
// Level 18
@ -524,7 +517,6 @@
{"type": "loop_cine"},
{"type": "exit_to_level", "level_id": 14},
],
"strings": {},
},
// Level 19
@ -553,7 +545,6 @@
{"type": "level_stats", "level_id": 14},
{"type": "exit_to_level", "level_id": 15},
],
"strings": {},
},
// Level 20
@ -573,7 +564,6 @@
{"type": "play_fmv", "fmv_path": "fmv/cafe.avi"},
{"type": "exit_to_title"},
],
"strings": {},
},
// Level 21
@ -586,7 +576,6 @@
"sequence": [
{"type": "exit_to_title"},
],
"strings": {},
},
],
@ -656,6 +645,8 @@
"INV_ITEM_SOUND": "Sound",
"INV_ITEM_UZI": "Uzis",
"INV_ITEM_UZI_AMMO": "Uzi Clips",
"ITEM_EXAMINE_ROLE": "\\{button empty} %s: Examine",
"ITEM_USE_ROLE": "\\{button empty} %s: Use",
"KEYMAP_ACTION": "Action",
"KEYMAP_BACK": "Back",
"KEYMAP_BILINEAR": "Toggle Texture Filter",
@ -766,6 +757,7 @@
"OSD_UI_OFF": "UI disabled",
"OSD_UI_ON": "UI enabled",
"OSD_UNKNOWN_COMMAND": "Unknown command: %s",
"PAGINATION_NAV": "%d / %d",
"PASSPORT_EXIT_GAME": "Exit Game",
"PASSPORT_EXIT_TO_TITLE": "Exit to Title",
"PASSPORT_LEGACY_SELECT_LEVEL_1": "Legacy saves do not",

View file

@ -62,7 +62,6 @@
"sequence": [
{"type": "exit_to_title"},
],
"strings": {},
},
// Level 4
@ -75,7 +74,6 @@
"sequence": [
{"type": "exit_to_title"},
],
"strings": {},
},
],
@ -145,6 +143,8 @@
"INV_ITEM_SOUND": "Sound",
"INV_ITEM_UZI": "Uzis",
"INV_ITEM_UZI_AMMO": "Uzi Clips",
"ITEM_EXAMINE_ROLE": "\\{button empty} %s: Examine",
"ITEM_USE_ROLE": "\\{button empty} %s: Use",
"KEYMAP_ACTION": "Action",
"KEYMAP_BACK": "Back",
"KEYMAP_BILINEAR": "Toggle Texture Filter",
@ -255,6 +255,7 @@
"OSD_UI_OFF": "UI disabled",
"OSD_UI_ON": "UI enabled",
"OSD_UNKNOWN_COMMAND": "Unknown command: %s",
"PAGINATION_NAV": "%d / %d",
"PASSPORT_EXIT_GAME": "Exit Game",
"PASSPORT_EXIT_TO_TITLE": "Exit to Title",
"PASSPORT_LEGACY_SELECT_LEVEL_1": "Legacy saves do not",

View file

@ -87,7 +87,6 @@
{"type": "level_stats", "level_id": 2},
{"type": "exit_to_level", "level_id": 3},
],
"strings": {},
"unobtainable_kills": 1,
},
@ -116,7 +115,6 @@
{"type": "total_stats", "picture_path": "data/images/install.webp"},
{"type": "exit_to_title"},
],
"strings": {},
},
// Level 4
@ -135,7 +133,6 @@
{"type": "play_fmv", "fmv_path": "fmv/escape.avi"},
{"type": "exit_to_title"},
],
"strings": {},
},
// Level 5
@ -151,7 +148,6 @@
"music": 0,
"inherit_injections": false,
"sequence": [{"type": "exit_to_title"}],
"strings": {},
},
],
@ -221,6 +217,8 @@
"INV_ITEM_SOUND": "Sound",
"INV_ITEM_UZI": "Uzis",
"INV_ITEM_UZI_AMMO": "Uzi Clips",
"ITEM_EXAMINE_ROLE": "\\{button empty} %s: Examine",
"ITEM_USE_ROLE": "\\{button empty} %s: Use",
"KEYMAP_ACTION": "Action",
"KEYMAP_BACK": "Back",
"KEYMAP_BILINEAR": "Toggle Texture Filter",
@ -331,6 +329,7 @@
"OSD_UI_OFF": "UI disabled",
"OSD_UI_ON": "UI enabled",
"OSD_UNKNOWN_COMMAND": "Unknown command: %s",
"PAGINATION_NAV": "%d / %d",
"PASSPORT_EXIT_GAME": "Exit Game",
"PASSPORT_EXIT_TO_TITLE": "Exit to Title",
"PASSPORT_LEGACY_SELECT_LEVEL_1": "Legacy saves do not",

View file

@ -4,6 +4,7 @@
- added an option to enable responsive swim cancellation, similar to TR2+ (#1004)
- added a special target, "pickup", to item-based console commands
- added support for custom levels to enforce values for any config setting (#1846)
- added support for key/puzzle/pickup descriptions, allowing players to examine said items in the inventory (#1821)
- changed OpenGL backend to use version 3.3, with fallback to 2.1 if initialization fails (#1738)
- changed text backend to accept named sequences. Currently supported sequences (limited by the sprites available in OG):
- `\{umlaut}`

View file

@ -251,6 +251,11 @@ Following are each of the properties available within a level.
"puzzle2": "Machine Cog",
// etc
},
"examine": {
"key1": "This shows when the player examines key1 in the inventory.",
"puzzle2": "You can use \n to make new lines and \f to make new pages.",
// etc
},
},
```
</details>
@ -295,6 +300,21 @@ Following are each of the properties available within a level.
for details.
</td>
</tr>
<tr valign="top">
<td>
<code>examine</code>
</td>
<td>String-to-string map</td>
<td>No</td>
<td colspan="2">
Allows longer text descriptions to be defined for key and puzzle items,
over and above the names defined in <code>strings</code> (see below).
Players can examine items in the inventory when this text has been
defined. Use <code>\n</code> in the text to create new lines; you can also
use <code>\f</code> to force a page break. Long text will be automatically
wrapped and paginated as necessary.
</td>
</tr>
<tr valign="top">
<td>
<code>file</code>
@ -372,10 +392,11 @@ Following are each of the properties available within a level.
<code>strings</code>
</td>
<td rowspan="12">String-to-string map</td>
<td rowspan="12">Yes</td>
<td rowspan="12">No</td>
<td colspan="2">
Key and puzzle item names. The possible types are as follows. An empty map
is permitted.
Key and puzzle item names. The possible types are as follows. If this map
is missing, or is missing an item's name, the game will use a suitable
default value.
</td>
</tr>
<tr valign="top">

View file

@ -10,6 +10,7 @@
typedef struct {
char *name;
char *description;
} M_NAME_ENTRY;
static M_NAME_ENTRY m_NamesTable[O_NUMBER_OF] = { 0 };
@ -21,6 +22,7 @@ static void M_ClearNames(void)
for (GAME_OBJECT_ID object_id = 0; object_id < O_NUMBER_OF; object_id++) {
M_NAME_ENTRY *const entry = &m_NamesTable[object_id];
Memory_FreePointer(&entry->name);
Memory_FreePointer(&entry->description);
}
}
@ -32,12 +34,27 @@ void Object_SetName(const GAME_OBJECT_ID object_id, const char *const name)
entry->name = Memory_DupStr(name);
}
void Object_SetDescription(
const GAME_OBJECT_ID object_id, const char *const description)
{
M_NAME_ENTRY *const entry = &m_NamesTable[object_id];
Memory_FreePointer(&entry->description);
assert(description != NULL);
entry->description = Memory_DupStr(description);
}
const char *Object_GetName(const GAME_OBJECT_ID object_id)
{
M_NAME_ENTRY *const entry = &m_NamesTable[object_id];
return entry != NULL ? entry->name : NULL;
}
const char *Object_GetDescription(GAME_OBJECT_ID object_id)
{
M_NAME_ENTRY *const entry = &m_NamesTable[object_id];
return entry != NULL ? entry->description : NULL;
}
void Object_ResetNames(void)
{
M_ClearNames();

View file

@ -1,6 +1,7 @@
#include "game/text.h"
#include "memory.h"
#include "utils.h"
#include <assert.h>
#include <string.h>
@ -278,20 +279,26 @@ int32_t Text_GetWidth(const TEXTSTRING *const text)
return 0;
}
int32_t width = 0;
const GLYPH_INFO **glyph_ptr = text->glyphs;
if (text->glyphs == NULL) {
return 0;
}
int32_t width = 0;
int32_t max_width = 0;
const GLYPH_INFO **glyph_ptr = text->glyphs;
while (*glyph_ptr != NULL) {
if ((*glyph_ptr)->role == GLYPH_SPACE) {
width += text->word_spacing;
} else if ((*glyph_ptr)->role == GLYPH_NEWLINE) {
max_width = MAX(max_width, width);
width = 0;
} else {
width += (*glyph_ptr)->width + text->letter_spacing;
}
glyph_ptr++;
}
width = MAX(max_width, width);
width -= text->letter_spacing;
return width * text->scale.h / TEXT_BASE_SCALE;
}

View file

@ -95,6 +95,12 @@ void UI_Label_SetSize(
self->height = height;
}
void UI_Label_SetVisible(UI_WIDGET *const widget, const bool visible)
{
UI_LABEL *const self = (UI_LABEL *)widget;
Text_Hide(self->text, !visible);
}
void UI_Label_AddFrame(UI_WIDGET *const widget)
{
UI_LABEL *const self = (UI_LABEL *)widget;
@ -141,3 +147,9 @@ int32_t UI_Label_MeasureTextWidth(UI_WIDGET *const widget)
UI_LABEL *const self = (UI_LABEL *)widget;
return Text_GetWidth(self->text);
}
int32_t UI_Label_MeasureTextHeight(UI_WIDGET *const widget)
{
UI_LABEL *const self = (UI_LABEL *)widget;
return Text_GetHeight(self->text);
}

View file

@ -6,10 +6,12 @@
#include <stdint.h>
const char *Object_GetName(GAME_OBJECT_ID object_id);
const char *Object_GetDescription(GAME_OBJECT_ID object_id);
void Object_ResetNames(void);
void Object_SetName(GAME_OBJECT_ID object_id, const char *name);
void Object_SetDescription(GAME_OBJECT_ID object_id, const char *description);
// Return a list of object ids that match given string.
// out_match_count may be NULL.

View file

@ -10,6 +10,7 @@ extern UI_WIDGET *UI_Label_Create(
extern void UI_Label_ChangeText(UI_WIDGET *widget, const char *text);
extern const char *UI_Label_GetText(UI_WIDGET *widget);
extern void UI_Label_SetSize(UI_WIDGET *widget, int32_t width, int32_t height);
extern void UI_Label_SetVisible(UI_WIDGET *widget, bool visible);
extern void UI_Label_AddFrame(UI_WIDGET *widget);
extern void UI_Label_RemoveFrame(UI_WIDGET *widget);
@ -17,3 +18,4 @@ extern void UI_Label_Flash(UI_WIDGET *widget, bool enable, int32_t rate);
extern void UI_Label_SetScale(UI_WIDGET *widget, float scale);
extern void UI_Label_SetZIndex(UI_WIDGET *widget, int32_t z_index);
extern int32_t UI_Label_MeasureTextWidth(UI_WIDGET *widget);
extern int32_t UI_Label_MeasureTextHeight(UI_WIDGET *widget);

View file

@ -1,5 +1,7 @@
#pragma once
#include "../vector.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
@ -15,4 +17,7 @@ bool String_ParseBool(const char *value, bool *target);
bool String_ParseInteger(const char *value, int32_t *target);
bool String_ParseDecimal(const char *value, float *target);
char *String_ToUpper(const char *text);
char *String_WordWrap(const char *text, size_t line_length);
VECTOR *String_Paginate(const char *text, int32_t max_lines);

View file

@ -8,6 +8,21 @@
#include <stdio.h>
#include <string.h>
static void M_AddPage(
const char *text, int32_t start_pos, int32_t length, VECTOR *pages);
static void M_AddPage(
const char *text, const int32_t start_pos, const int32_t length,
VECTOR *const pages)
{
char substr[length + 1];
strncpy(substr, text + start_pos, length);
substr[length] = '\0';
const char *page = Memory_DupStr(substr);
Vector_Add(pages, &page);
}
bool String_EndsWith(const char *str, const char *suffix)
{
int str_len = strlen(str);
@ -157,6 +172,25 @@ bool String_ParseDecimal(const char *const value, float *const target)
return true;
}
char *String_ToUpper(const char *text)
{
if (text == NULL) {
return NULL;
}
const size_t text_len = strlen(text);
char *const upper_text = Memory_Alloc(text_len + 1);
char *dest = upper_text;
while (*text != '\0') {
*dest++ = toupper(*text++);
}
*dest = '\0';
return upper_text;
}
char *String_WordWrap(const char *text, const size_t line_len)
{
if (text == NULL || line_len == 0) {
@ -231,3 +265,37 @@ char *String_WordWrap(const char *text, const size_t line_len)
*dest = '\0';
return wrapped_text;
}
VECTOR *String_Paginate(const char *const text, const int32_t max_lines)
{
VECTOR *const pages = Vector_Create(sizeof(char *));
int32_t line_count = 0;
int32_t start_pos = 0;
int32_t current_length = 0;
const char *iter_text = text;
while (*iter_text != '\0') {
current_length++;
if (*iter_text == '\n') {
line_count++;
}
if (line_count == max_lines || *iter_text == '\f') {
M_AddPage(text, start_pos, current_length, pages);
start_pos += current_length;
current_length = 0;
line_count = 0;
}
*iter_text++;
}
// Anything that is left becomes its own page.
if (pages->count == 0 || current_length != 0) {
M_AddPage(text, start_pos, current_length, pages);
}
return pages;
}

View file

@ -98,6 +98,7 @@ typedef struct {
bool fix_animated_sprites;
bool enable_skybox;
bool enable_ps1_crystals;
bool enable_item_examining;
struct {
int32_t layout;

View file

@ -113,3 +113,4 @@ CFG_BOOL(g_Config, enable_skybox, true)
CFG_BOOL(g_Config, enable_ps1_crystals, true)
CFG_BOOL(g_Config, ui.enable_game_ui, true)
CFG_BOOL(g_Config, ui.enable_photo_mode_ui, true)
CFG_BOOL(g_Config, enable_item_examining, true)

View file

@ -141,3 +141,6 @@ GS_DEFINE(PHOTO_MODE_RESET_PROMPT, "Reset camera")
GS_DEFINE(PHOTO_MODE_SNAP_PROMPT, "Take picture")
GS_DEFINE(PHOTO_MODE_ROLL_ROLE, "Sidestep L/R")
GS_DEFINE(PHOTO_MODE_FOV_ROLE, "Draw <+Walk>")
GS_DEFINE(ITEM_EXAMINE_ROLE, "\\{button empty} %s: Examine")
GS_DEFINE(ITEM_USE_ROLE, "\\{button empty} %s: Use")
GS_DEFINE(PAGINATION_NAV, "%d / %d")

View file

@ -60,7 +60,10 @@ static bool M_LoadLevelSequence(JSON_OBJECT *obj, int32_t level_num);
static bool M_LoadScriptLevels(JSON_OBJECT *obj);
static void M_StringTableShutdown(GAMEFLOW_STRING_ENTRY *dest);
static bool M_LoadObjectNames(
JSON_OBJECT *root_obj, GAMEFLOW_STRING_ENTRY **dest);
JSON_OBJECT *root_obj, const char *key, GAMEFLOW_STRING_ENTRY **dest);
static void M_ScanLevelText(
const GAMEFLOW_STRING_ENTRY *entry,
void (*callback)(GAME_OBJECT_ID object_id, const char *text));
static const STRING_TO_ENUM_TYPE m_GameflowLevelTypeEnumMap[] = {
{ "title", GFL_TITLE },
@ -531,11 +534,11 @@ static void M_StringTableShutdown(GAMEFLOW_STRING_ENTRY *const dest)
}
static bool M_LoadObjectNames(
JSON_OBJECT *const root_obj, GAMEFLOW_STRING_ENTRY **dest)
JSON_OBJECT *const root_obj, const char *const key,
GAMEFLOW_STRING_ENTRY **dest)
{
JSON_OBJECT *strings_obj = JSON_ObjectGetObject(root_obj, "strings");
JSON_OBJECT *strings_obj = JSON_ObjectGetObject(root_obj, key);
if (strings_obj == NULL) {
LOG_ERROR("'strings' must be a dictionary");
return false;
}
@ -543,6 +546,7 @@ static bool M_LoadObjectNames(
char *json_key;
char *target_string;
} legacy_string_defs[] = {
// clang-format off
{ "puzzle1", "O_PUZZLE_ITEM_1" },
{ "puzzle1", "O_PUZZLE_OPTION_1" },
{ "puzzle2", "O_PUZZLE_ITEM_2" },
@ -563,7 +567,11 @@ static bool M_LoadObjectNames(
{ "pickup1", "O_PICKUP_OPTION_1" },
{ "pickup2", "O_PICKUP_ITEM_2" },
{ "pickup2", "O_PICKUP_OPTION_2" },
{ "leadbar", "O_LEADBAR_ITEM" },
{ "leadbar", "O_LEADBAR_OPTION" },
{ "scion", "O_SCION_OPTION" },
{ NULL, NULL },
// clang-format on
};
// count allocation size
@ -593,6 +601,20 @@ static bool M_LoadObjectNames(
return true;
}
static void M_ScanLevelText(
const GAMEFLOW_STRING_ENTRY *entry,
void (*callback)(GAME_OBJECT_ID object_id, const char *text))
{
while (entry != NULL && entry->key != NULL) {
const GAME_OBJECT_ID object_id =
ENUM_MAP_GET(GAME_OBJECT_ID, entry->key, NO_OBJECT);
if (object_id != NO_OBJECT) {
callback(object_id, entry->value);
}
entry++;
}
}
static bool M_LoadScriptLevels(JSON_OBJECT *obj)
{
JSON_ARRAY *jlvl_arr = JSON_ObjectGetArray(obj, "levels");
@ -747,7 +769,8 @@ static bool M_LoadScriptLevels(JSON_OBJECT *obj)
cur->unobtainable.secrets =
JSON_ObjectGetInt(jlvl_obj, "unobtainable_secrets", 0);
M_LoadObjectNames(jlvl_obj, &cur->object_strings);
M_LoadObjectNames(jlvl_obj, "strings", &cur->object_strings);
M_LoadObjectNames(jlvl_obj, "examine", &cur->examine_strings);
tmp_i = JSON_ObjectGetBool(jlvl_obj, "inherit_injections", 1);
tmp_arr = JSON_ObjectGetArray(jlvl_obj, "injections");
@ -921,6 +944,7 @@ void GameFlow_Shutdown(void)
if (g_GameFlow.levels) {
for (int i = 0; i < g_GameFlow.level_count; i++) {
M_StringTableShutdown(g_GameFlow.levels[i].object_strings);
M_StringTableShutdown(g_GameFlow.levels[i].examine_strings);
for (int j = 0; j < g_GameFlow.levels[i].injections.length; j++) {
Memory_FreePointer(
@ -1477,15 +1501,8 @@ void GameFlow_LoadStrings(int32_t level_num)
if (level_num >= 0) {
assert(level_num < g_GameFlow.level_count);
const GAMEFLOW_LEVEL *const level = &g_GameFlow.levels[level_num];
const GAMEFLOW_STRING_ENTRY *entry = level->object_strings;
while (entry != NULL && entry->key != NULL) {
const GAME_OBJECT_ID object_id =
ENUM_MAP_GET(GAME_OBJECT_ID, entry->key, NO_OBJECT);
if (object_id != NO_OBJECT) {
Object_SetName(object_id, entry->value);
}
entry++;
}
M_ScanLevelText(level->object_strings, Object_SetName);
M_ScanLevelText(level->examine_strings, Object_SetDescription);
}
}

View file

@ -27,6 +27,7 @@ typedef struct {
char *level_title;
char *level_file;
GAMEFLOW_STRING_ENTRY *object_strings;
GAMEFLOW_STRING_ENTRY *examine_strings;
int8_t demo;
GAMEFLOW_SEQUENCE *sequence;
struct {

View file

@ -4,6 +4,7 @@
#include "game/game_string.h"
#include "game/inventory.h"
#include "game/inventory/inventory_vars.h"
#include "game/option/option_examine.h"
#include "game/output.h"
#include "game/overlay.h"
#include "game/text.h"
@ -25,6 +26,26 @@ static TEXTSTRING *m_InvDownArrow1 = NULL;
static TEXTSTRING *m_InvDownArrow2 = NULL;
static TEXTSTRING *m_InvUpArrow1 = NULL;
static TEXTSTRING *m_InvUpArrow2 = NULL;
static TEXTSTRING *m_ExamineItemText = NULL;
static TEXTSTRING *m_UseItemText = NULL;
static TEXTSTRING *M_InitExamineText(
int32_t x_pos, const char *role_str, const char *input_str);
static TEXTSTRING *M_InitExamineText(
const int32_t x_pos, const char *const role_str,
const char *const input_str)
{
char role[100];
sprintf(role, role_str, input_str);
TEXTSTRING *const text = Text_Create(x_pos, -100, role);
Text_AlignBottom(text, true);
Text_CentreH(text, true);
Text_Hide(text, true);
return text;
}
void Inv_Ring_Init(
RING_INFO *ring, int16_t type, INVENTORY_ITEM **list, int16_t qty,
@ -77,6 +98,37 @@ void Inv_Ring_Init(
ring->light.z = 1024;
}
bool Inv_Ring_CanExamine(void)
{
return g_Config.enable_item_examining && m_ExamineItemText != NULL
&& !m_ExamineItemText->flags.hide;
}
void Inv_Ring_InitExamineOverlay(void)
{
if ((g_InvMode != INV_GAME_MODE && g_InvMode != INV_KEYS_MODE)
|| !g_Config.enable_item_examining || m_ExamineItemText != NULL) {
return;
}
m_ExamineItemText =
M_InitExamineText(-100, GS(ITEM_EXAMINE_ROLE), GS(KEYMAP_LOOK));
m_UseItemText =
M_InitExamineText(100, GS(ITEM_USE_ROLE), GS(KEYMAP_ACTION));
}
void Inv_Ring_RemoveExamineOverlay(void)
{
if (m_ExamineItemText == NULL) {
return;
}
Text_Remove(m_ExamineItemText);
Text_Remove(m_UseItemText);
m_ExamineItemText = NULL;
m_UseItemText = NULL;
}
void Inv_Ring_InitHeader(RING_INFO *ring)
{
if (g_InvMode == INV_TITLE_MODE) {
@ -155,6 +207,7 @@ void Inv_Ring_RemoveHeader(void)
void Inv_Ring_RemoveAllText(void)
{
Inv_Ring_RemoveHeader();
Inv_Ring_RemoveExamineOverlay();
for (int i = 0; i < IT_NUMBER_OF; i++) {
if (g_InvItemText[i]) {
Text_Remove(g_InvItemText[i]);
@ -176,6 +229,8 @@ void Inv_Ring_Active(INVENTORY_ITEM *inv_item)
char temp_text[128];
int32_t qty = Inv_RequestItem(inv_item->object_id);
bool show_examine_option = false;
switch (inv_item->object_id) {
case O_SHOTGUN_OPTION:
if (!g_InvItemText[IT_QTY] && !(g_GameInfo.bonus_flag & GBF_NGPLUS)) {
@ -279,6 +334,9 @@ void Inv_Ring_Active(INVENTORY_ITEM *inv_item)
Text_AlignBottom(g_InvItemText[IT_QTY], 1);
Text_CentreH(g_InvItemText[IT_QTY], 1);
}
show_examine_option = !Option_Examine_IsActive()
&& Option_Examine_CanExamine(inv_item->object_id);
break;
default:
@ -304,6 +362,11 @@ void Inv_Ring_Active(INVENTORY_ITEM *inv_item)
Text_Hide(m_InvDownArrow2, false);
g_GameInfo.inv_showing_medpack = false;
}
if (m_ExamineItemText != NULL) {
Text_Hide(m_ExamineItemText, !show_examine_option);
Text_Hide(m_UseItemText, !show_examine_option);
}
}
void Inv_Ring_ResetItem(INVENTORY_ITEM *const inv_item)
@ -316,6 +379,7 @@ void Inv_Ring_ResetItem(INVENTORY_ITEM *const inv_item)
inv_item->y_rot = 0;
inv_item->ytrans = 0;
inv_item->ztrans = 0;
inv_item->action = ACTION_USE;
if (inv_item->object_id == O_PASSPORT_OPTION) {
inv_item->object_id = O_PASSPORT_CLOSED;
}

View file

@ -14,6 +14,10 @@ void Inv_Ring_RemoveAllText(void);
void Inv_Ring_Active(INVENTORY_ITEM *inv_item);
void Inv_Ring_ResetItem(INVENTORY_ITEM *inv_item);
bool Inv_Ring_CanExamine(void);
void Inv_Ring_InitExamineOverlay(void);
void Inv_Ring_RemoveExamineOverlay(void);
void Inv_Ring_GetView(RING_INFO *ring, XYZ_32 *view_pos, XYZ_16 *view_rot);
void Inv_Ring_Light(RING_INFO *ring);
void Inv_Ring_CalcAdders(RING_INFO *ring, int16_t rotation_duration);

View file

@ -4,10 +4,12 @@
#include "game/option/option_compass.h"
#include "game/option/option_controls.h"
#include "game/option/option_controls_pick.h"
#include "game/option/option_examine.h"
#include "game/option/option_graphics.h"
#include "game/option/option_passport.h"
#include "game/option/option_sound.h"
#include "global/types.h"
#include "global/vars.h"
static CONTROL_MODE m_ControlMode = CM_PICK;
@ -38,6 +40,21 @@ void Option_Shutdown(INVENTORY_ITEM *inv_item)
}
break;
case O_PICKUP_OPTION_1:
case O_PICKUP_OPTION_2:
case O_PUZZLE_OPTION_1:
case O_PUZZLE_OPTION_2:
case O_PUZZLE_OPTION_3:
case O_PUZZLE_OPTION_4:
case O_KEY_OPTION_1:
case O_KEY_OPTION_2:
case O_KEY_OPTION_3:
case O_KEY_OPTION_4:
case O_SCION_OPTION:
case O_LEADBAR_OPTION:
Option_Examine_Shutdown();
break;
default:
break;
}
@ -89,6 +106,17 @@ void Option_Control(INVENTORY_ITEM *inv_item)
case O_EXPLOSIVE_OPTION:
case O_MEDI_OPTION:
case O_BIGMEDI_OPTION:
g_InputDB.menu_confirm = 1;
break;
case O_PISTOL_AMMO_OPTION:
case O_SG_AMMO_OPTION:
case O_MAG_AMMO_OPTION:
case O_UZI_AMMO_OPTION:
break;
case O_PICKUP_OPTION_1:
case O_PICKUP_OPTION_2:
case O_PUZZLE_OPTION_1:
case O_PUZZLE_OPTION_2:
case O_PUZZLE_OPTION_3:
@ -97,16 +125,13 @@ void Option_Control(INVENTORY_ITEM *inv_item)
case O_KEY_OPTION_2:
case O_KEY_OPTION_3:
case O_KEY_OPTION_4:
case O_PICKUP_OPTION_1:
case O_PICKUP_OPTION_2:
case O_SCION_OPTION:
g_InputDB.menu_confirm = 1;
break;
case O_PISTOL_AMMO_OPTION:
case O_SG_AMMO_OPTION:
case O_MAG_AMMO_OPTION:
case O_UZI_AMMO_OPTION:
case O_LEADBAR_OPTION:
if (inv_item->action == ACTION_EXAMINE) {
Option_Examine_Control(inv_item->object_id);
} else {
g_InputDB.menu_confirm = 1;
}
break;
default:
@ -134,6 +159,21 @@ void Option_Draw(INVENTORY_ITEM *inv_item)
}
break;
case O_PICKUP_OPTION_1:
case O_PICKUP_OPTION_2:
case O_PUZZLE_OPTION_1:
case O_PUZZLE_OPTION_2:
case O_PUZZLE_OPTION_3:
case O_PUZZLE_OPTION_4:
case O_KEY_OPTION_1:
case O_KEY_OPTION_2:
case O_KEY_OPTION_3:
case O_KEY_OPTION_4:
case O_SCION_OPTION:
case O_LEADBAR_OPTION:
Option_Examine_Draw();
break;
default:
break;
}

View file

@ -0,0 +1,58 @@
#include "game/option/option_examine.h"
#include "game/input.h"
#include "game/ui/widgets/paginator.h"
#include <libtrx/game/objects/names.h>
#include <libtrx/game/ui/common.h>
#define MAX_LINES 10
static UI_WIDGET *m_PaginatorUI = NULL;
static void M_End(void);
static void M_End(void)
{
m_PaginatorUI->free(m_PaginatorUI);
m_PaginatorUI = NULL;
}
bool Option_Examine_CanExamine(const GAME_OBJECT_ID object_id)
{
return Object_GetDescription(object_id) != NULL;
}
bool Option_Examine_IsActive(void)
{
return m_PaginatorUI != NULL;
}
void Option_Examine_Control(const GAME_OBJECT_ID object_id)
{
if (m_PaginatorUI == NULL) {
m_PaginatorUI = UI_Paginator_Create(
Object_GetName(object_id), Object_GetDescription(object_id),
MAX_LINES);
}
m_PaginatorUI->control(m_PaginatorUI);
if (g_InputDB.menu_back || g_InputDB.menu_confirm) {
M_End();
}
}
void Option_Examine_Draw(void)
{
if (m_PaginatorUI != NULL) {
m_PaginatorUI->draw(m_PaginatorUI);
}
}
void Option_Examine_Shutdown(void)
{
if (m_PaginatorUI != NULL) {
M_End();
}
}

View file

@ -0,0 +1,9 @@
#pragma once
#include "global/types.h"
bool Option_Examine_CanExamine(GAME_OBJECT_ID object_id);
bool Option_Examine_IsActive(void);
void Option_Examine_Control(GAME_OBJECT_ID object_id);
void Option_Examine_Draw(void);
void Option_Examine_Shutdown(void);

View file

@ -771,7 +771,8 @@ static PHASE_CONTROL M_ControlFrame(void)
g_InputDB = (INPUT_STATE) { 0 };
}
if (g_InputDB.menu_confirm) {
const bool examine = g_InputDB.look && Inv_Ring_CanExamine();
if (g_InputDB.menu_confirm || examine) {
if ((g_InvMode == INV_SAVE_MODE
|| g_InvMode == INV_SAVE_CRYSTAL_MODE
|| g_InvMode == INV_LOAD_MODE || g_InvMode == INV_DEATH_MODE)
@ -795,6 +796,7 @@ static PHASE_CONTROL M_ControlFrame(void)
inv_item->goal_frame = inv_item->open_frame;
inv_item->anim_direction = 1;
inv_item->action = examine ? ACTION_EXAMINE : ACTION_USE;
Inv_Ring_MotionSetup(
ring, RNG_SELECTING, RNG_SELECTED, SELECTING_FRAMES);
@ -1088,8 +1090,10 @@ static PHASE_CONTROL M_ControlFrame(void)
Inv_Ring_Active(inv_item);
}
Inv_Ring_InitHeader(ring);
Inv_Ring_InitExamineOverlay();
} else {
Inv_Ring_RemoveHeader();
Inv_Ring_RemoveExamineOverlay();
}
if (!motion->status || motion->status == RNG_CLOSING

View file

@ -0,0 +1,228 @@
#include "game/ui/widgets/paginator.h"
#include "game/game_string.h"
#include "game/input.h"
#include "game/screen.h"
#include "game/sound.h"
#include "game/text.h"
#include <libtrx/game/ui/common.h>
#include <libtrx/game/ui/widgets/label.h>
#include <libtrx/game/ui/widgets/spacer.h>
#include <libtrx/game/ui/widgets/stack.h>
#include <libtrx/game/ui/widgets/window.h>
#include <libtrx/memory.h>
#include <libtrx/strings/common.h>
#include <stdio.h>
#define TITLE_MARGIN 5
#define WINDOW_MARGIN 10
#define DIALOG_PADDING 5
#define PADDING_SCALED (3.5 * (DIALOG_PADDING + WINDOW_MARGIN))
typedef struct {
UI_WIDGET_VTABLE vtable;
UI_WIDGET *window;
UI_WIDGET *outer_stack;
UI_WIDGET *bottom_stack;
UI_WIDGET *title;
UI_WIDGET *top_spacer;
UI_WIDGET *text;
UI_WIDGET *bottom_spacer;
UI_WIDGET *left_arrow;
UI_WIDGET *right_arrow;
UI_WIDGET *right_arrow_spacer;
UI_WIDGET *page_label;
int32_t current_page;
VECTOR *page_content;
} UI_PAGINATOR;
static void M_DoLayout(UI_PAGINATOR *self);
static int32_t M_GetWidth(const UI_PAGINATOR *self);
static int32_t M_GetHeight(const UI_PAGINATOR *self);
static void M_SetPosition(UI_PAGINATOR *self, int32_t x, int32_t y);
static bool M_SelectPage(UI_PAGINATOR *const self, int32_t new_page);
static void M_Control(UI_PAGINATOR *self);
static void M_Draw(UI_PAGINATOR *self);
static void M_Free(UI_PAGINATOR *self);
static void M_DoLayout(UI_PAGINATOR *const self)
{
M_SetPosition(
self, (Screen_GetResWidthDownscaled(RSR_TEXT) - M_GetWidth(self)) / 2.0,
(Screen_GetResHeightDownscaled(RSR_TEXT) - M_GetHeight(self)) / 2.0);
}
static int32_t M_GetWidth(const UI_PAGINATOR *const self)
{
return self->window->get_width(self->window);
}
static int32_t M_GetHeight(const UI_PAGINATOR *const self)
{
return self->window->get_height(self->window);
}
static void M_SetPosition(UI_PAGINATOR *const self, int32_t x, int32_t y)
{
return self->window->set_position(self->window, x, y);
}
static bool M_SelectPage(UI_PAGINATOR *const self, const int32_t new_page)
{
if (new_page == self->current_page || new_page < 0
|| new_page >= self->page_content->count) {
return false;
}
self->current_page = new_page;
UI_Label_ChangeText(
self->text,
*(char **)Vector_Get(self->page_content, self->current_page));
char page_indicator[100];
sprintf(
page_indicator, GS(PAGINATION_NAV), self->current_page + 1,
self->page_content->count);
UI_Label_ChangeText(self->page_label, page_indicator);
UI_Label_SetVisible(self->left_arrow, self->current_page > 0);
UI_Label_SetVisible(
self->right_arrow, self->current_page < self->page_content->count - 1);
return true;
}
static void M_Control(UI_PAGINATOR *const self)
{
const int32_t page_shift = g_InputDB.left ? -1 : (g_InputDB.right ? 1 : 0);
if (M_SelectPage(self, self->current_page + page_shift)) {
Sound_Effect(SFX_MENU_PASSPORT, NULL, SPM_ALWAYS);
}
if (self->window->control != NULL) {
self->window->control(self->window);
}
}
static void M_Draw(UI_PAGINATOR *const self)
{
if (self->window->draw != NULL) {
self->window->draw(self->window);
}
}
static void M_Free(UI_PAGINATOR *const self)
{
for (int32_t i = self->page_content->count - 1; i >= 0; i--) {
char *const page = *(char **)Vector_Get(self->page_content, i);
Memory_Free(page);
}
Vector_Free(self->page_content);
self->text->free(self->text);
self->top_spacer->free(self->top_spacer);
self->title->free(self->title);
if (self->bottom_stack != NULL) {
self->bottom_spacer->free(self->bottom_spacer);
self->left_arrow->free(self->left_arrow);
self->right_arrow->free(self->right_arrow);
self->right_arrow_spacer->free(self->right_arrow_spacer);
self->page_label->free(self->page_label);
self->bottom_stack->free(self->bottom_stack);
}
self->outer_stack->free(self->outer_stack);
self->window->free(self->window);
Memory_Free(self);
}
UI_WIDGET *UI_Paginator_Create(
const char *const title, const char *const text, const int32_t max_lines)
{
UI_PAGINATOR *const self = Memory_Alloc(sizeof(UI_PAGINATOR));
self->vtable = (UI_WIDGET_VTABLE) {
.control = (UI_WIDGET_CONTROL)M_Control,
.draw = (UI_WIDGET_DRAW)M_Draw,
.get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth,
.get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight,
.set_position = (UI_WIDGET_SET_POSITION)M_SetPosition,
.free = (UI_WIDGET_FREE)M_Free,
};
self->outer_stack = UI_Stack_Create(
UI_STACK_LAYOUT_VERTICAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE);
const char *upper_title = String_ToUpper(title);
self->title =
UI_Label_Create(upper_title, UI_LABEL_AUTO_SIZE, TEXT_HEIGHT_FIXED);
Memory_FreePointer(&upper_title);
UI_Stack_AddChild(self->outer_stack, self->title);
self->top_spacer = UI_Spacer_Create(TITLE_MARGIN, TITLE_MARGIN);
UI_Stack_AddChild(self->outer_stack, self->top_spacer);
const char *wrapped =
String_WordWrap(text, Text_GetMaxLineLength() - PADDING_SCALED);
self->page_content = String_Paginate(wrapped, max_lines);
self->current_page = 0;
Memory_FreePointer(&wrapped);
self->text = UI_Label_Create(
*(char **)Vector_Get(self->page_content, 0), UI_LABEL_AUTO_SIZE,
UI_LABEL_AUTO_SIZE);
UI_Stack_AddChild(self->outer_stack, self->text);
if (self->page_content->count > 1) {
self->bottom_spacer = UI_Spacer_Create(TITLE_MARGIN, TITLE_MARGIN * 3);
UI_Stack_AddChild(self->outer_stack, self->bottom_spacer);
self->bottom_stack = UI_Stack_Create(
UI_STACK_LAYOUT_HORIZONTAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE);
UI_Stack_AddChild(self->outer_stack, self->bottom_stack);
self->left_arrow =
UI_Label_Create("\\{button left}", 22, TEXT_HEIGHT_FIXED);
self->right_arrow =
UI_Label_Create("\\{button right}", 16, TEXT_HEIGHT_FIXED);
self->right_arrow_spacer = UI_Spacer_Create(6, TEXT_HEIGHT_FIXED);
self->page_label =
UI_Label_Create("", UI_LABEL_AUTO_SIZE, UI_LABEL_AUTO_SIZE);
UI_Stack_AddChild(self->bottom_stack, self->left_arrow);
UI_Stack_AddChild(self->bottom_stack, self->page_label);
UI_Stack_AddChild(self->bottom_stack, self->right_arrow_spacer);
UI_Stack_AddChild(self->bottom_stack, self->right_arrow);
UI_Stack_SetHAlign(self->bottom_stack, UI_STACK_H_ALIGN_RIGHT);
}
self->window = UI_Window_Create(
self->outer_stack, DIALOG_PADDING, DIALOG_PADDING, DIALOG_PADDING * 2,
DIALOG_PADDING);
// Ensure minimum width for page navigation as text content may be empty.
int32_t max_width =
MAX(self->page_content->count == 1 ? 0 : 100,
UI_Label_MeasureTextWidth(self->title));
int32_t max_nav_width = 0;
int32_t max_height = 0;
for (int32_t i = 0; i < self->page_content->count; i++) {
M_SelectPage(self, i);
max_width = MAX(max_width, UI_Label_MeasureTextWidth(self->text));
max_height = MAX(max_height, UI_Label_MeasureTextHeight(self->text));
if (self->bottom_stack != NULL) {
max_nav_width =
MAX(max_nav_width, UI_Label_MeasureTextWidth(self->page_label));
}
}
UI_Label_SetSize(self->text, max_width, max_height);
if (self->bottom_stack != NULL) {
UI_Stack_SetSize(self->bottom_stack, max_width, UI_STACK_AUTO_SIZE);
UI_Label_SetSize(self->page_label, max_nav_width, UI_LABEL_AUTO_SIZE);
}
M_SelectPage(self, 0);
M_DoLayout(self);
return (UI_WIDGET *)self;
}

View file

@ -0,0 +1,6 @@
#pragma once
#include <libtrx/game/ui/widgets/base.h>
UI_WIDGET *UI_Paginator_Create(
const char *title, const char *text, int32_t max_lines);

View file

@ -791,6 +791,11 @@ typedef struct {
int16_t sprnum;
} INVENTORY_SPRITE;
typedef enum {
ACTION_USE = 0,
ACTION_EXAMINE = 1,
} INVENTORY_ITEM_ACTION;
typedef struct {
GAME_OBJECT_ID object_id;
int16_t frames_total;
@ -812,6 +817,7 @@ typedef struct {
uint32_t drawn_meshes;
int16_t inv_pos;
INVENTORY_SPRITE **sprlist;
INVENTORY_ITEM_ACTION action;
} INVENTORY_ITEM;
typedef struct {

View file

@ -107,6 +107,7 @@ sources = [
'game/effect_routines/earthquake.c',
'game/effect_routines/floor_shake.c',
'game/ui/common.c',
'game/ui/widgets/paginator.c',
'game/ui/widgets/photo_mode.c',
'game/effect_routines/explosion.c',
'game/effect_routines/finish_level.c',
@ -241,6 +242,7 @@ sources = [
'game/option/option_compass.c',
'game/option/option_controls.c',
'game/option/option_controls_pick.c',
'game/option/option_examine.c',
'game/option/option_graphics.c',
'game/option/option_passport.c',
'game/option/option_sound.c',

View file

@ -292,6 +292,10 @@
"Title": "Timer counts in inventory",
"Description": "Makes the in-game timer work even while the game is showing the inventory."
},
"enable_item_examining": {
"Title": "Item examination",
"Description": "For custom levels - allows item descriptions to be displayed in the inventory where the level author has provided suitable data."
},
"anisotropy_filter": {
"Title": "Anisotropic filter",
"Description": "Enhances texture filtering at distances."

View file

@ -296,6 +296,10 @@
"Title": "Temporizador en el inventario",
"Description": "Permite que el temporizador del juego siga funcionando incluso cuando se muestra el inventario."
},
"enable_item_examining": {
"Title": "Examen del artículo",
"Description": "Para niveles personalizados: permite que se muestren descripciones de elementos en el inventario donde el autor del nivel haya proporcionado datos adecuados."
},
"enable_total_stats": {
"Title": "Pantalla de estadísticas finales",
"Description": "Habilita una pantalla de estadísticas totales del juego que se reproduce después de los créditos."

View file

@ -292,6 +292,10 @@
"Title": "Compteur de temps de jeu dans l'inventaire",
"Description": "Fait fonctionner le conteur du jeu même en étant dans les menus de l'inventaire."
},
"enable_item_examining": {
"Title": "Examen des articles",
"Description": "Pour les niveaux personnalisés - permet d'afficher les descriptions d'éléments dans l'inventaire lorsque l'auteur du niveau a fourni les données appropriées."
},
"anisotropy_filter": {
"Title": "Filtre anisotropique",
"Description": "Améliore le filtrage des textures à distance."

View file

@ -292,6 +292,10 @@
"Title": "Conta tempo di gioco nell'inventario",
"Description": "Fa in modo che il tempo di gioco scorra anche nel menu dell'inventario"
},
"enable_item_examining": {
"Title": "Esame dell'oggetto",
"Description": "Per livelli personalizzati: consente di visualizzare le descrizioni degli oggetti nell'inventario in cui l'autore del livello ha fornito dati adeguati."
},
"anisotropy_filter": {
"Title": "Filtro anisotropico",
"Description": "Migliora il filtraggio delle texture a distanza."

View file

@ -368,6 +368,11 @@
"Field": "enable_timer_in_inventory",
"DataType": "Bool",
"DefaultValue": true
},
{
"Field": "enable_item_examining",
"DataType": "Bool",
"DefaultValue": true
}
]
},

View file

@ -30,7 +30,7 @@ def process_tr1() -> None:
gameflow: str, strings_map: dict[str, str]
) -> str:
gameflow = re.sub(
r'^( "strings": {)[^}]*(})',
r'^( "strings": {)[\S\s]*?^( })',
' "strings": {\n'
+ "\n".join(
f" {json.dumps(key)}: {json.dumps(value)},"
@ -64,7 +64,7 @@ def process_tr2() -> None:
game_strings_map: dict[str, str],
) -> str:
gameflow = re.sub(
r'^( "object_strings": {)[^}]*(})',
r'^( "object_strings": {)[\S\s]*?^( })',
' "object_strings": {\n'
+ "\n".join(
f" {json.dumps(key)}: {json.dumps(value)},"
@ -76,7 +76,7 @@ def process_tr2() -> None:
)
gameflow = re.sub(
r'^( "game_strings": {)[^}]*(})',
r'^( "game_strings": {)[\S\s]*?^( })',
' "game_strings": {\n'
+ "\n".join(
f" {json.dumps(key)}: {json.dumps(value)},"