tr2/options: add graphic options dialog

Resolves #1615.
This commit is contained in:
Marcin Kurczewski 2025-04-10 00:06:07 +02:00
parent 613353be0b
commit 4f58f8bc9c
No known key found for this signature in database
GPG key ID: CC65E6FD28CAE42A
14 changed files with 415 additions and 10 deletions

View file

@ -364,9 +364,9 @@
"DETAIL_RENDER_MODE_LEGACY": "Window size",
"DETAIL_RESOLUTION": "Resolution",
"DETAIL_RESOLUTION_FMT": "%dx%d",
"DETAIL_SELECT_DETAIL": "Select Detail",
"DETAIL_STRING_FMT": "%s",
"DETAIL_TEXTURE_FILTER": "Texture filter",
"DETAIL_TITLE": "Graphic Options",
"DETAIL_TRAPEZOID_FILTER": "Trapezoid filter",
"DETAIL_UI_BAR_SCALE": "UI bar scale",
"DETAIL_UI_TEXT_SCALE": "UI text scale",

View file

@ -472,6 +472,10 @@
"CONTROL_CUSTOM_3": "User Keys 3",
"CONTROL_DEFAULT_KEYS": "Default Keys",
"DETAIL_FLOAT_FMT": "%.1f",
"DETAIL_FOG_END": "Fog end",
"DETAIL_FOG_START": "Fog start",
"DETAIL_INTEGER_FMT": "%d",
"DETAIL_TITLE": "Graphic Options",
"HEADING_GAME_OVER": "GAME OVER",
"HEADING_INVENTORY": "INVENTORY",
"HEADING_ITEMS": "ITEMS",

View file

@ -8,6 +8,7 @@
PS1 | 77 | 255 | 255 | ![#4DFFFF](https://placehold.co/15x15/4DFFFF/4DFFFF.png) `#4DFFFF`
DOS | 153 | 179 | 255 | ![#99B3FF](https://placehold.co/15x15/99B3FF/99B3FF.png) `#99B3FF`
- changed the `draw_distance_min` and `draw_distance_max` to `fog_start` and `fog_end`
- changed `Select Detail` dialog title to `Graphic Options`
- fixed the bilinear filter to not readjust the UVs (#2258)
- fixed anisotropy filter causing black lines on certain GPUs (#902)
- fixed mesh faces not being drawn under some circumstances (#2452, #2438)

View file

@ -1,5 +1,6 @@
## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-0.10...develop) - ××××-××-××
- added support for The Golden Mask (#1621)
- added sunglasses for graphic options (#1615)
- added control over the fog distances for players and level builders
- added an installer for Windows (#2681)
- added the bonus level game flow type, which allows for levels to be unlocked if all main game secrets are found (#2668)

View file

@ -163,6 +163,7 @@ as Notepad.
- added support for more accented characters
- added fade effects to displayed images
- added a wireframe mode
- added sunglasses for graphic options
- improved support for windowed mode
#### Gameplay

View file

@ -127,4 +127,8 @@ GS_DEFINE(PASSPORT_EXIT_TO_TITLE, "Exit to Title")
GS_DEFINE(SOUND_SET_VOLUMES, "Set Volumes")
GS_DEFINE(OSD_TRAPEZOID_FILTER_ON, "Trapezoid filter enabled")
GS_DEFINE(OSD_TRAPEZOID_FILTER_OFF, "Trapezoid filter disabled")
GS_DEFINE(DETAIL_INTEGER_FMT, "%d")
GS_DEFINE(DETAIL_FLOAT_FMT, "%.1f")
GS_DEFINE(DETAIL_TITLE, "Graphic Options")
GS_DEFINE(DETAIL_FOG_START, "Fog start")
GS_DEFINE(DETAIL_FOG_END, "Fog end")

View file

@ -7,10 +7,7 @@ GS_DEFINE(PASSPORT_MODE_NEW_GAME, "New Game")
GS_DEFINE(PASSPORT_MODE_NEW_GAME_PLUS, "New Game+")
GS_DEFINE(PASSPORT_MODE_NEW_GAME_JP, "Japanese NG")
GS_DEFINE(PASSPORT_MODE_NEW_GAME_JP_PLUS, "Japanese NG+")
GS_DEFINE(DETAIL_SELECT_DETAIL, "Select Detail")
GS_DEFINE(DETAIL_FPS, "FPS")
GS_DEFINE(DETAIL_FOG_START, "Fog start")
GS_DEFINE(DETAIL_FOG_END, "Fog end")
GS_DEFINE(DETAIL_WATER_COLOR_R, "Water color (R)")
GS_DEFINE(DETAIL_WATER_COLOR_G, "Water color (G)")
GS_DEFINE(DETAIL_WATER_COLOR_B, "Water color (B)")
@ -27,7 +24,6 @@ GS_DEFINE(DETAIL_RENDER_MODE, "Render mode")
GS_DEFINE(DETAIL_RENDER_MODE_LEGACY, "Window size")
GS_DEFINE(DETAIL_RENDER_MODE_FBO, "Framebuffer")
GS_DEFINE(DETAIL_RESOLUTION, "Resolution")
GS_DEFINE(DETAIL_INTEGER_FMT, "%d")
GS_DEFINE(DETAIL_STRING_FMT, "%s")
GS_DEFINE(DETAIL_RESOLUTION_FMT, "%dx%d")
GS_DEFINE(CONTROL_RESET_DEFAULTS, "Reset All: Hold %s")

View file

@ -203,7 +203,7 @@ static void M_InitText(void)
Text_CentreH(m_Text[TEXT_TITLE_BORDER], 1);
Text_CentreV(m_Text[TEXT_TITLE_BORDER], 1);
m_Text[TEXT_TITLE] = Text_Create(0, TOP_Y, GS(DETAIL_SELECT_DETAIL));
m_Text[TEXT_TITLE] = Text_Create(0, TOP_Y, GS(DETAIL_TITLE));
Text_CentreH(m_Text[TEXT_TITLE], 1);
Text_CentreV(m_Text[TEXT_TITLE], 1);
Text_AddBackground(m_Text[TEXT_TITLE], ROW_WIDTH - 4, 0, 0, 0, TS_HEADING);

View file

@ -32,8 +32,8 @@
#include <stdio.h>
#define TITLE_RING_OBJECTS 3
#define OPTION_RING_OBJECTS 3
#define TITLE_RING_OBJECTS 4
#define OPTION_RING_OBJECTS 4
#define INV_RING_FADE_TIME_FAST \
(INV_RING_CLOSE_FRAMES / INV_RING_FRAMES / (double)LOGIC_FPS)
#define INV_RING_FADE_TIME_TITLE_FINISH 0.25

View file

@ -44,11 +44,12 @@ INV_RING_SOURCE g_InvRing_Source[RT_NUMBER_OF] = {
},
},
[RT_OPTION] = {
.count = 4,
.count = 5,
.current = 0,
.qtys = { 1, 1, 1, 1 },
.qtys = { 1, 1, 1, 1, 1 },
.items = {
&g_InvRing_Item_Passport,
&g_InvRing_Item_Graphics,
&g_InvRing_Item_Controls,
&g_InvRing_Item_Sound,
&g_InvRing_Item_Photo,

View file

@ -1,16 +1,43 @@
#include "game/input.h"
#include "game/option/option.h"
#include "game/text.h"
#include "game/ui/widgets/graphics_dialog.h"
#include "global/vars.h"
static struct {
UI_WIDGET *widget;
} m_Priv = {};
static void M_ConstructUI(void);
static void M_DestroyUI(void);
static void M_ConstructUI(void)
{
m_Priv.widget = UI_GraphicsDialog_Create();
}
static void M_DestroyUI(void)
{
if (m_Priv.widget != nullptr) {
m_Priv.widget->free(m_Priv.widget);
m_Priv.widget = nullptr;
}
}
void Option_Detail_Control(INVENTORY_ITEM *const item, const bool is_busy)
{
if (m_Priv.widget == nullptr) {
M_ConstructUI();
}
m_Priv.widget->control(m_Priv.widget);
}
void Option_Detail_Draw(INVENTORY_ITEM *const item)
{
m_Priv.widget->draw(m_Priv.widget);
}
void Option_Detail_Shutdown(void)
{
M_DestroyUI();
}

View file

@ -0,0 +1,364 @@
#include "game/ui/widgets/graphics_dialog.h"
#include <libtrx/config.h>
#include <libtrx/game/game_string.h>
#include <libtrx/game/input.h>
#include <libtrx/game/text.h>
#include <libtrx/game/ui/common.h>
#include <libtrx/game/ui/widgets/frame.h>
#include <libtrx/game/ui/widgets/label.h>
#include <libtrx/game/ui/widgets/stack.h>
#include <libtrx/game/ui/widgets/window.h>
#include <libtrx/log.h>
#include <libtrx/memory.h>
#include <libtrx/strings.h>
typedef struct {
CONFIG_OPTION_TYPE option_type;
GAME_STRING_ID label_id;
void *target;
int32_t min_value;
int32_t max_value;
} M_OPTION;
typedef struct {
void *user_data;
UI_WIDGET *frame;
UI_WIDGET *stack;
UI_WIDGET *title_label;
UI_WIDGET *value_label;
UI_WIDGET *arrow_left_label;
UI_WIDGET *arrow_right_label;
} M_ROW;
typedef struct {
UI_WIDGET_VTABLE vtable;
UI_WIDGET *window;
UI_WIDGET *outer_stack;
int32_t visible_rows;
bool is_confirmed;
int32_t selected_row_offset;
int32_t visible_row_offset;
int32_t row_count;
M_ROW *rows;
int32_t selection_margin;
int32_t selection_padding;
int32_t listener;
} M_WIDGET;
static M_OPTION m_Options[] = {
{
.option_type = COT_INT32,
.label_id = GS_ID(DETAIL_FOG_END),
.target = &g_Config.visuals.fog_start,
.min_value = 1,
.max_value = 100,
},
{
.option_type = COT_INT32,
.label_id = GS_ID(DETAIL_FOG_START),
.target = &g_Config.visuals.fog_end,
.min_value = 1,
.max_value = 100,
},
{
.target = nullptr,
},
};
static void M_ClearRows(M_WIDGET *self);
static char *M_FormatRowValue(int32_t row_idx);
static bool M_CanChangeValue(int32_t row_idx, int32_t delta);
static bool M_RequestChangeValue(
M_WIDGET *self, int32_t row_idx, int32_t delta);
static void M_DeselectRow(M_WIDGET *self, int32_t row_idx);
static void M_SelectRow(M_WIDGET *self, int32_t row_idx);
static M_ROW *M_AddRow(
M_WIDGET *self, const char *left_text, const char *right_text,
void *user_data);
static void M_DoLayout(M_WIDGET *self);
static void M_HandleCanvasResize(const EVENT *event, void *data);
static int32_t M_GetWidth(const M_WIDGET *self);
static int32_t M_GetHeight(const M_WIDGET *self);
static void M_SetPosition(M_WIDGET *self, int32_t x, int32_t y);
static void M_Control(M_WIDGET *self);
static void M_Draw(M_WIDGET *self);
static void M_Free(M_WIDGET *self);
static void M_ClearRows(M_WIDGET *const self)
{
for (int32_t i = 0; i < self->row_count; i++) {
self->rows[i].title_label->free(self->rows[i].title_label);
self->rows[i].value_label->free(self->rows[i].value_label);
self->rows[i].arrow_left_label->free(self->rows[i].arrow_left_label);
self->rows[i].arrow_right_label->free(self->rows[i].arrow_right_label);
self->rows[i].frame->free(self->rows[i].frame);
self->rows[i].stack->free(self->rows[i].stack);
}
UI_Stack_ClearChildren(self->outer_stack);
self->visible_row_offset = 0;
self->row_count = 0;
self->selected_row_offset = -1;
self->is_confirmed = false;
}
static char *M_FormatRowValue(const int32_t row_idx)
{
const M_OPTION *const option = &m_Options[row_idx];
switch (option->option_type) {
case COT_INT32:
return String_Format(
GS(DETAIL_INTEGER_FMT), *(int32_t *)option->target);
break;
default:
break;
}
return nullptr;
}
static bool M_CanChangeValue(const int32_t row_idx, const int32_t delta)
{
const M_OPTION *const option = &m_Options[row_idx];
switch (option->option_type) {
case COT_INT32:
if (delta < 0) {
return *(int32_t *)option->target > option->min_value;
} else if (delta > 0) {
return *(int32_t *)option->target < option->max_value;
}
break;
default:
return false;
}
return false;
}
static bool M_RequestChangeValue(
M_WIDGET *const self, const int32_t row_idx, const int32_t delta)
{
if (!M_CanChangeValue(row_idx, delta)) {
return false;
}
const M_OPTION *const option = &m_Options[row_idx];
switch (option->option_type) {
case COT_INT32:
*(int32_t *)option->target += delta;
break;
default:
return false;
}
M_ROW *const row = &self->rows[row_idx];
char *value_text = M_FormatRowValue(row_idx);
UI_Label_ChangeText(row->value_label, value_text);
UI_Label_SetVisible(row->arrow_left_label, M_CanChangeValue(row_idx, -1));
UI_Label_SetVisible(row->arrow_right_label, M_CanChangeValue(row_idx, +1));
Memory_Free(value_text);
Config_Write();
return true;
}
static void M_DeselectRow(M_WIDGET *const self, const int32_t row_idx)
{
M_ROW *const row = &self->rows[row_idx];
UI_Label_SetVisible(row->arrow_left_label, false);
UI_Label_SetVisible(row->arrow_right_label, false);
UI_Frame_SetFrameVisible(row->frame, false);
}
static void M_SelectRow(M_WIDGET *const self, const int32_t row_idx)
{
M_ROW *const row = &self->rows[row_idx];
UI_Label_SetVisible(row->arrow_left_label, M_CanChangeValue(row_idx, -1));
UI_Label_SetVisible(row->arrow_right_label, M_CanChangeValue(row_idx, +1));
UI_Frame_SetFrameVisible(row->frame, true);
}
static M_ROW *M_AddRow(
M_WIDGET *const self, const char *const left_text,
const char *const right_text, void *const user_data)
{
self->row_count++;
self->rows = Memory_Realloc(self->rows, sizeof(M_ROW) * self->row_count);
M_ROW *const row = &self->rows[self->row_count - 1];
row->stack =
UI_Stack_Create(UI_STACK_LAYOUT_HORIZONTAL, 200, UI_STACK_AUTO_SIZE);
UI_Stack_SetHAlign(row->stack, UI_STACK_H_ALIGN_DISTRIBUTE);
row->frame = UI_Frame_Create(row->stack, 0, 0);
UI_Frame_SetFrameVisible(row->frame, false);
row->title_label = UI_Label_Create(left_text, 120, UI_LABEL_AUTO_SIZE);
UI_Stack_AddChild(row->stack, row->title_label);
row->arrow_left_label = UI_Label_Create(
"\\{button left} ", UI_LABEL_AUTO_SIZE, UI_LABEL_AUTO_SIZE);
UI_Label_SetVisible(row->arrow_left_label, false);
UI_Stack_AddChild(row->stack, row->arrow_left_label);
row->value_label =
UI_Label_Create(right_text, UI_LABEL_AUTO_SIZE, UI_LABEL_AUTO_SIZE);
UI_Stack_AddChild(row->stack, row->value_label);
row->arrow_right_label = UI_Label_Create(
" \\{button right}", UI_LABEL_AUTO_SIZE, UI_LABEL_AUTO_SIZE);
UI_Label_SetVisible(row->arrow_right_label, false);
UI_Stack_AddChild(row->stack, row->arrow_right_label);
row->user_data = user_data;
for (int32_t y = 0; y < self->row_count; y++) {
self->rows[y].stack->is_hidden = y < self->visible_row_offset
|| y >= self->visible_row_offset + self->visible_rows;
}
if (self->selected_row_offset == -1) {
self->selected_row_offset = 0;
M_SelectRow(self, self->selected_row_offset);
}
UI_Stack_AddChild(self->outer_stack, row->frame);
return row;
}
static void M_DoLayout(M_WIDGET *const self)
{
M_SetPosition(
self, (UI_GetCanvasWidth() - M_GetWidth(self)) / 2,
(UI_GetCanvasHeight() - M_GetHeight(self)) * 2 / 3);
}
static void M_HandleCanvasResize(const EVENT *event, void *data)
{
M_WIDGET *const self = (M_WIDGET *)data;
M_DoLayout(self);
}
static int32_t M_GetWidth(const M_WIDGET *const self)
{
return self->window->get_width(self->window);
}
static int32_t M_GetHeight(const M_WIDGET *const self)
{
return self->window->get_height(self->window);
}
static void M_SetPosition(
M_WIDGET *const self, const int32_t x, const int32_t y)
{
self->window->set_position(self->window, x, y);
}
static void M_Control(M_WIDGET *const self)
{
if (self->window->control != nullptr) {
self->window->control(self->window);
}
bool update = false;
if (g_InputDB.menu_down) {
if (self->visible_row_offset + self->visible_rows < self->row_count) {
self->rows[self->visible_row_offset].stack->is_hidden = true;
self->rows[self->visible_row_offset + self->visible_rows]
.stack->is_hidden = false;
self->visible_row_offset++;
update = true;
}
} else if (g_InputDB.menu_up) {
if (self->visible_row_offset > 0) {
self->rows[self->visible_row_offset + self->visible_rows - 1]
.stack->is_hidden = true;
self->rows[self->visible_row_offset - 1].stack->is_hidden = false;
self->visible_row_offset--;
update = true;
}
}
if (g_InputDB.menu_down
&& self->selected_row_offset + 1 < self->row_count) {
if (self->selected_row_offset != -1) {
M_DeselectRow(self, self->selected_row_offset);
}
self->selected_row_offset++;
M_SelectRow(self, self->selected_row_offset);
update = true;
} else if (g_InputDB.menu_up && self->selected_row_offset > 0) {
if (self->selected_row_offset != -1) {
M_DeselectRow(self, self->selected_row_offset);
}
self->selected_row_offset--;
M_SelectRow(self, self->selected_row_offset);
update = true;
}
if (g_InputDB.menu_left && self->selected_row_offset >= 0) {
M_RequestChangeValue(self, self->selected_row_offset, -1);
} else if (g_InputDB.menu_right && self->selected_row_offset >= 0) {
M_RequestChangeValue(self, self->selected_row_offset, +1);
}
if (g_InputDB.menu_confirm) {
self->is_confirmed = true;
}
if (update) {
M_DoLayout(self);
}
}
static void M_Draw(M_WIDGET *const self)
{
if (self->window->draw != nullptr) {
self->window->draw(self->window);
}
}
static void M_Free(M_WIDGET *const self)
{
M_ClearRows(self);
self->outer_stack->free(self->outer_stack);
self->window->free(self->window);
UI_Events_Unsubscribe(self->listener);
Memory_Free(self);
}
UI_WIDGET *UI_GraphicsDialog_Create(void)
{
M_WIDGET *const self = Memory_Alloc(sizeof(M_WIDGET));
self->vtable = (UI_WIDGET_VTABLE) {
.get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth,
.get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight,
.set_position = (UI_WIDGET_SET_POSITION)M_SetPosition,
.control = (UI_WIDGET_CONTROL)M_Control,
.draw = (UI_WIDGET_DRAW)M_Draw,
.free = (UI_WIDGET_FREE)M_Free,
};
self->visible_rows = 2;
self->outer_stack = UI_Stack_Create(
UI_STACK_LAYOUT_VERTICAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE);
UI_Stack_SetHAlign(self->outer_stack, UI_STACK_H_ALIGN_CENTER);
self->window = UI_Window_Create(self->outer_stack, 8, 8, 8, 8);
UI_Window_SetTitle(self->window, GS(DETAIL_TITLE));
self->selected_row_offset = -1;
self->listener = UI_Events_Subscribe(
"canvas_resize", nullptr, M_HandleCanvasResize, self);
for (int32_t i = 0; m_Options[i].target != nullptr; i++) {
char *value_text = M_FormatRowValue(i);
M_AddRow(
self, GameString_Get(m_Options[i].label_id), value_text, nullptr);
Memory_Free(value_text);
}
M_DoLayout(self);
return (UI_WIDGET *)self;
}

View file

@ -0,0 +1,5 @@
#pragma once
#include <libtrx/game/ui/widgets/base.h>
UI_WIDGET *UI_GraphicsDialog_Create(void);

View file

@ -283,6 +283,7 @@ sources = [
'game/ui/widgets/controls_input_selector.c',
'game/ui/widgets/controls_layout_editor.c',
'game/ui/widgets/controls_layout_selector.c',
'game/ui/widgets/graphics_dialog.c',
'game/ui/widgets/stats_dialog.c',
'game/viewport.c',
'global/enum_map.c',