Add in-game settings menu

This commit is contained in:
saulo benigno committed 2026-10-03 19:07:13 -03:00
1 parent 933aac6175
commit 31e54eba1b
8 files changed
+536 -15

No files matched your search

+13
View File
@@ -22,6 +22,19 @@ the full screen, the frame rate, the volumes, internet play, the
multiplayer HUD), the profile's controller settings, and the keyboard and
mouse's controls.
In a game, the pause menus are the maps' own. The widgets named `in_game/...`
(`ce/in_game.xml`, also from `tools/port_settings.py`) are built in the game's
maps instead of `ui.map`, with only what they lead to (the screens they open,
and their bitmaps and strings): they are drawn with the pause menus' art,
which `ui.map` has not. `in_game/settings_button` is SETTINGS, which the full
screen pause menus (the campaign's, and a network game's with one player
here) get after RESUME GAME (`source/interface/ui_widget.c`), on the
desktop. Its screen opens Edit Profile's Controls Setup, Gamepads, Mouse
Setup, Audio Setup and Video Setup over the pause menu's dim. They save to
`config.toml` as they do from the main menu, but Gamepads, whose changes
Edit Profile saves there, saves only its controller settings to the
player's active profile, on OK (`port active profile edit begin` and `end`).
To change the menus without building the game, put files in a `menus` folder
next to `config.toml`. A file with the same path as one here replaces it. On
the desktop, any other `.xml` file there is added too. A PNG that a `<frame>`
+63
View File
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The port's SETTINGS in the game's pause menus (tools/port_settings.py) -->
<menus>
<widget name="in_game/settings_button" text="SETTINGS" type="text" width="202" height="27" bitmap="ui\shell\bitmaps\menu_bkds" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3" platform="desktop">
<on event="a" open="in_game/settings_screen"/>
<on event="start" open="in_game/settings_screen"/>
</widget>
<widget name="in_game/settings_screen" width="640" height="480" flags="pass_unhandled_to_focused_child pause_game" bitmap="ui\shell\bitmaps\semi_transparent_grey" platform="desktop">
<on event="b" back="true"/>
<on event="back" back="true"/>
<child widget="in_game/settings_box" x="210" y="164"/>
<child widget="in_game/settings_list" x="218" y="170"/>
<child widget="ui\shell\main_menu\button_key_sm" x="235" y="298"/>
</widget>
<widget name="in_game/settings_box" width="226" height="154" platform="desktop">
<child widget="ui\shell\solo_game\pause_game\pausebox2_left" x="-4"/>
<child widget="ui\shell\solo_game\pause_game\pausebox2_left_center" x="12"/>
<child widget="ui\shell\solo_game\pause_game\pausebox2_right" x="206"/>
</widget>
<widget name="in_game/settings_list" type="column_list" width="202" height="119" flags="pass_unhandled_to_focused_child up_down_tabs_items" platform="desktop">
<child widget="in_game/controls_setup_button" y="0"/>
<child widget="in_game/gamepads_button" y="23"/>
<child widget="in_game/mouse_setup_button" y="46"/>
<child widget="in_game/audio_setup_button" y="69"/>
<child widget="in_game/video_setup_button" y="92"/>
</widget>
<widget name="in_game/controls_setup_button" string_list="main_menu/settings_select/player_setup/player_profile_edit/profile_edit_options" string_index="1" type="text" width="202" height="27" bitmap="ui\shell\bitmaps\menu_bkds" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3" platform="desktop">
<on event="a" open="in_game/controls_setup_screen"/>
<on event="start" open="in_game/controls_setup_screen"/>
</widget>
<widget name="in_game/controls_setup_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="ui\shell\bitmaps\semi_transparent_grey" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/controls_setup/controls_settings_screen"/>
</widget>
<widget name="in_game/gamepads_button" string_list="main_menu/settings_select/player_setup/player_profile_edit/profile_edit_options" string_index="2" type="text" width="202" height="27" bitmap="ui\shell\bitmaps\menu_bkds" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3" platform="desktop">
<on event="a" run="port active profile edit begin" open="in_game/gamepads_screen"/>
<on event="start" run="port active profile edit begin" open="in_game/gamepads_screen"/>
</widget>
<widget name="in_game/gamepads_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="ui\shell\bitmaps\semi_transparent_grey" platform="desktop">
<on event="deleted" run="port active profile edit end"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/gamepad_setup/gamepad_setup_screen"/>
</widget>
<widget name="in_game/mouse_setup_button" string_list="main_menu/settings_select/player_setup/player_profile_edit/profile_edit_options" string_index="3" type="text" width="202" height="27" bitmap="ui\shell\bitmaps\menu_bkds" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3" platform="desktop">
<on event="a" open="in_game/mouse_setup_screen"/>
<on event="start" open="in_game/mouse_setup_screen"/>
</widget>
<widget name="in_game/mouse_setup_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="ui\shell\bitmaps\semi_transparent_grey" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/mouse_settings_screen"/>
</widget>
<widget name="in_game/audio_setup_button" string_list="main_menu/settings_select/player_setup/player_profile_edit/profile_edit_options" string_index="4" type="text" width="202" height="27" bitmap="ui\shell\bitmaps\menu_bkds" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3" platform="desktop">
<on event="a" open="in_game/audio_setup_screen"/>
<on event="start" open="in_game/audio_setup_screen"/>
</widget>
<widget name="in_game/audio_setup_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="ui\shell\bitmaps\semi_transparent_grey" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/audio_settings_screen"/>
</widget>
<widget name="in_game/video_setup_button" string_list="main_menu/settings_select/player_setup/player_profile_edit/profile_edit_options" string_index="5" type="text" width="202" height="27" bitmap="ui\shell\bitmaps\menu_bkds" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3" platform="desktop">
<on event="a" open="in_game/video_setup_screen"/>
<on event="start" open="in_game/video_setup_screen"/>
</widget>
<widget name="in_game/video_setup_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="ui\shell\bitmaps\semi_transparent_grey" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/video_settings_screen"/>
</widget>
</menus>
+1
View File
@@ -3,6 +3,7 @@
"files": [
"ce/bitmaps.xml",
"ce/error.xml",
"ce/in_game.xml",
"ce/main_menu.campaign_select.xml",
"ce/main_menu.difficulty_select.xml",
"ce/main_menu.gametype_select.xml",
+69
View File
@@ -10,6 +10,9 @@ calls them from PC_MENU_FUNCTION_BASE on):
- the PC version's "profile set edit begin" begins editing the first player
profile, as its settings screens need, and fails with none (its handlers
then open the screens that make one);
- in a game, "port active profile edit begin" and "port active profile edit
end" edit the player's active profile for GAMEPADS (in_game.xml) and keep
the controller settings it sets;
- "gamespy screen init" hides the server browser's error and filter panels,
and the title of the mode "mp type set mode" did not choose (Internet or
LAN);
@@ -480,6 +483,64 @@ boolean pc_menu_profile_edit_begin(void)
return TRUE;
}
/* in a game, GAMEPADS (port/assets/menus/ce/in_game.xml) sets the player's
active profile, which no Edit Profile screen is there to save: "port active
profile edit begin" (A on its row) edits the profile, and "port active
profile edit end" (its screen gone) puts the controller settings OK wrote
into the profile as it is stored and as the game has it, nothing else (the
game's copy can be older than the file: Edit Profile saves only the file),
then saves the one and applies the other, as the game's own pause menu does
with its inverted look (player0_look_invert_pitch). FALSE if the player has
no profile. */
static long active_profile_edited = NONE;
static boolean active_profile_edit_begin(short local_player)
{
long index = player_ui_get_active_player_profile_index(local_player);
active_profile_edited = NONE;
if (index == NONE)
return FALSE;
player_ui_begin_editing_profile(index);
if (!player_ui_get_edit_player_profile())
return FALSE;
active_profile_edited = index;
return TRUE;
}
static boolean active_profile_edit_end(short local_player)
{
struct player_profile *edited = player_ui_get_edit_player_profile();
long index = active_profile_edited;
active_profile_edited = NONE;
if (index == NONE)
return TRUE;
if (edited && player_ui_edit_profile_is_dirty())
{
struct player_profile profile;
if (player_profile_get(index, &profile))
{
profile.controller_settings = edited->controller_settings;
player_profile_save(index, &profile);
if (player_ui_get_active_player_profile_index(local_player) == index)
{
player_ui_get_active_player_profile(local_player, &profile);
profile.controller_settings = edited->controller_settings;
player_ui_set_active_player_profile(local_player, index, &profile);
}
platform_log("menus: the profile's controller settings saved");
}
else
{
platform_log("menus: could not read the profile; its controller settings not saved");
}
}
player_ui_end_editing_profile();
return TRUE;
}
/* ---------- the campaign */
/* the rows of its lists (main_menu/new_select/list_item_0 to 10) */
@@ -3386,6 +3447,14 @@ boolean pc_menu_event_function_invoke(
{
return pc_menu_profile_edit_begin();
}
else if (!strcmp(name, "port active profile edit begin"))
{
return active_profile_edit_begin(controller_of(widget));
}
else if (!strcmp(name, "port active profile edit end"))
{
return active_profile_edit_end(controller_of(widget));
}
else if (!strcmp(name, "mouse emit accept event"))
{
event_manager_post_button(controller_of(widget), BUTTON_A);
+142 -14
View File
@@ -23,6 +23,11 @@ game keeps its own menus.
The tag table cannot grow where it is (the tags' names follow it), so it
is copied, with ours after it; every existing tag keeps its index. All of it
is let go in scenario_tags_unload, before the next map's tags load.
The game's maps get the in-game widgets (in_game/..., which ui.map has not:
they are drawn with the pause menus' art, which only the game's maps have),
and only what they lead to: the settings screens they open, with their
bitmaps and strings (ui_widget.c adds SETTINGS to the pause menus).
*/
#include "cseries.h"
@@ -58,6 +63,7 @@ boolean pc_menu_tag(long tag_index);
/* ---------- constants */
#define PC_MENU_TAG_PREFIX "pc\\"
#define IN_GAME_WIDGET_PREFIX "in_game/"
#define UI_WIDGET_DEFINITION_TAG 'DeLa'
#define UNICODE_STRING_LIST_TAG 'ustr'
#define FONT_TAG 'font'
@@ -334,6 +340,8 @@ static char const *const port_function_names[] =
"mp profile init indicator opts", "mp profile set ctf rules", "mp profile set koth rules",
"mp profile set slayer rules", "mp profile set oddball rules", "mp profile set racing rules",
"mp profile set player options", "mp profile set item options", "mp profile set indicator opts",
/* (in a game: GAMEPADS on the player's active profile) */
"port active profile edit begin", "port active profile edit end",
};
/* the PC version's game data functions that the Xbox's have not, from
@@ -365,6 +373,8 @@ static struct
struct pc_menu_setting *settings;
long setting_count;
boolean loaded;
/* built in a game's map: the in-game widgets, not the main menu */
boolean in_game;
char root[300];
} menu_tags;
@@ -382,6 +392,10 @@ static struct
long *strings_tags;
long *text_tags;
long *spinner_tags;
/* which widgets, bitmaps and string lists are built */
boolean *wanted_widgets;
boolean *wanted_bitmaps;
boolean *wanted_strings;
boolean failed;
} build;
@@ -492,6 +506,70 @@ static long widget_named(char const *name)
return find(name, build.menus->widget_count, widget_name);
}
static boolean in_game_widget(char const *name)
{
return !strncmp(name, IN_GAME_WIDGET_PREFIX, strlen(IN_GAME_WIDGET_PREFIX));
}
/* one of ours by its name (not the map's, with a backslash) wanted */
static void want_named(char const *name, long count, char const *(*name_of)(long), boolean *wanted)
{
long index = name && !strchr(name, '\\') ? find(name, count, name_of) : NONE;
if (index != NONE)
wanted[index] = TRUE;
}
static void want_widget(long index);
static void want_widget_named(char const *name)
{
long index = name && !strchr(name, '\\') ? widget_named(name) : NONE;
if (index != NONE)
want_widget(index);
}
/* a widget wanted, and what it names: its bitmaps and strings, its children,
its description, the widgets its handlers open and its conditional ones */
static void want_widget(long index)
{
struct halo_menus const *menus = build.menus;
struct halo_menu_widget const *widget = &menus->widgets[index];
long child, handler, conditional;
if (build.wanted_widgets[index])
return;
build.wanted_widgets[index] = TRUE;
want_named(widget->bitmap, menus->bitmap_count, bitmap_name, build.wanted_bitmaps);
want_named(widget->header_bitmap, menus->bitmap_count, bitmap_name, build.wanted_bitmaps);
want_named(widget->footer_bitmap, menus->bitmap_count, bitmap_name, build.wanted_bitmaps);
want_named(widget->string_list, menus->string_list_count, strings_name, build.wanted_strings);
want_widget_named(widget->description);
for (child = widget->first_child; child != HALO_MENU_NONE; child = menus->children[child].next)
{
if (menus->children[child].nested != HALO_MENU_NONE)
want_widget(menus->children[child].nested);
else
want_widget_named(menus->children[child].widget);
}
for (handler = widget->first_handler; handler != HALO_MENU_NONE; handler = menus->handlers[handler].next)
{
struct halo_menu_handler const *on = &menus->handlers[handler];
want_widget_named(on->open);
want_widget_named(on->replace);
want_widget_named(on->focus);
want_widget_named(on->widget);
want_widget_named(on->otherwise);
}
for (conditional = widget->first_conditional; conditional != HALO_MENU_NONE;
conditional = menus->conditionals[conditional].next)
{
want_widget_named(menus->conditionals[conditional].widget);
}
}
static long map_tag(long group_tag, char const *name, char const *file, long line)
{
long index = tag_loaded(group_tag, name);
@@ -1255,8 +1333,11 @@ void menu_tags_loaded(
struct halo_menus const *menus;
struct cache_file_tag_instance *instances;
long widget_count, own_lists = 0, total, index;
long widgets_built = 0, strings_built = 0, bitmaps_built = 0;
/* (any map but ui.map is a game's) */
boolean in_game = strcmp(map_name, "ui") != 0;
if (strcmp(map_name, "ui") || strcmp(config_string("display.menus"), "pc"))
if (strcmp(config_string("display.menus"), "pc"))
return;
menus = halo_menus_load();
if (!menus)
@@ -1269,33 +1350,64 @@ void menu_tags_loaded(
build.spinner_tags = malloc((widget_count + 1) * sizeof(long));
build.bitmap_tags = malloc((menus->bitmap_count + 1) * sizeof(long));
build.strings_tags = malloc((menus->string_list_count + 1) * sizeof(long));
build.wanted_widgets = malloc((widget_count + 1) * sizeof(boolean));
build.wanted_bitmaps = malloc((menus->bitmap_count + 1) * sizeof(boolean));
build.wanted_strings = malloc((menus->string_list_count + 1) * sizeof(boolean));
/* ui.map: all but the in-game widgets; a game's map: those, and what
they lead to */
for (index = 0; index < widget_count; index++)
build.wanted_widgets[index] = !in_game && !in_game_widget(menus->widgets[index].name);
for (index = 0; index < menus->bitmap_count; index++)
build.wanted_bitmaps[index] = !in_game;
for (index = 0; index < menus->string_list_count; index++)
build.wanted_strings[index] = !in_game;
for (index = 0; in_game && index < widget_count; index++)
{
if (in_game_widget(menus->widgets[index].name))
want_widget(index);
}
for (index = 0; index < widget_count; index++)
{
own_lists += (menus->widgets[index].text != NULL) + (menus->widgets[index].strings != NULL);
if (build.wanted_widgets[index])
{
widgets_built++;
own_lists += (menus->widgets[index].text != NULL) + (menus->widgets[index].strings != NULL);
}
/* (names are unique) */
if (widget_named(menus->widgets[index].name) != index)
problem(menus->widgets[index].file, menus->widgets[index].line, "two widgets are named",
menus->widgets[index].name);
}
total = widget_count + own_lists + menus->string_list_count + menus->bitmap_count;
for (index = 0; index < menus->string_list_count; index++)
strings_built += build.wanted_strings[index] != FALSE;
for (index = 0; index < menus->bitmap_count; index++)
bitmaps_built += build.wanted_bitmaps[index] != FALSE;
/* (a game's map with no in-game widgets: nothing to add) */
if (!widgets_built && !build.failed)
goto done;
total = widgets_built + own_lists + strings_built + bitmaps_built;
instances = build.failed ? NULL : instances_grow(total, &build.first_index, &build.first_salt);
if (!instances)
goto failed;
/* each new tag's index */
for (index = 0; index < widget_count; index++)
{
build.widget_tags[index] = next_tag();
build.text_tags[index] = menus->widgets[index].text ? next_tag() : NONE;
build.spinner_tags[index] = menus->widgets[index].strings ? next_tag() : NONE;
boolean wanted = build.wanted_widgets[index];
build.widget_tags[index] = wanted ? next_tag() : NONE;
build.text_tags[index] = wanted && menus->widgets[index].text ? next_tag() : NONE;
build.spinner_tags[index] = wanted && menus->widgets[index].strings ? next_tag() : NONE;
}
for (index = 0; index < menus->string_list_count; index++)
build.strings_tags[index] = next_tag();
build.strings_tags[index] = build.wanted_strings[index] ? next_tag() : NONE;
for (index = 0; index < menus->bitmap_count; index++)
build.bitmap_tags[index] = next_tag();
build.bitmap_tags[index] = build.wanted_bitmaps[index] ? next_tag() : NONE;
/* the tags: the bitmaps and strings first, which the widgets name; the
widgets' names in the table before any is built, which they find there */
for (index = 0; index < menus->bitmap_count && !build.failed; index++)
{
if (!build.wanted_bitmaps[index])
continue;
instance_set(instances, BITMAP_GROUP_TAG, build.bitmap_tags[index], menus->bitmaps[index].name, "",
bitmap_build(&menus->bitmaps[index], build.bitmap_tags[index]));
}
@@ -1303,6 +1415,8 @@ void menu_tags_loaded(
{
struct halo_menu_strings const *list = &menus->string_lists[index];
if (!build.wanted_strings[index])
continue;
instance_set(instances, UNICODE_STRING_LIST_TAG, build.strings_tags[index], list->name, "",
string_list_build(menus->strings + list->first, list->count));
}
@@ -1310,6 +1424,8 @@ void menu_tags_loaded(
{
struct halo_menu_widget const *widget = &menus->widgets[index];
if (!build.wanted_widgets[index])
continue;
if (widget->text)
{
instance_set(instances, UNICODE_STRING_LIST_TAG, build.text_tags[index], widget->name, " text",
@@ -1329,10 +1445,17 @@ void menu_tags_loaded(
goto failed;
cache_files_set_tag_instances(instances, build.first_index + total);
for (index = 0; index < widget_count && !build.failed; index++)
instances[DATUM_INDEX_TO_ABSOLUTE_INDEX(build.widget_tags[index])].base_address = widget_build(index);
{
if (build.wanted_widgets[index])
instances[DATUM_INDEX_TO_ABSOLUTE_INDEX(build.widget_tags[index])].base_address = widget_build(index);
}
if (build.failed)
goto failed;
if (widget_named(menus->root) != NONE)
if (in_game)
{
/* (no main menu here) */
}
else if (widget_named(menus->root) != NONE)
{
snprintf(menu_tags.root, sizeof(menu_tags.root), "%s%s", PC_MENU_TAG_PREFIX, menus->root);
for (index = 0; menu_tags.root[index]; index++)
@@ -1346,12 +1469,14 @@ void menu_tags_loaded(
platform_log("menus: there is no widget named %s, the main menu", menus->root);
}
menu_tags.loaded = TRUE;
menu_tags.in_game = in_game;
platform_log("menus: %ld widgets, %ld string lists and %ld bitmaps added to the map's %ld tags",
widget_count, own_lists + menus->string_list_count, menus->bitmap_count, build.first_index);
widgets_built, own_lists + strings_built, bitmaps_built, build.first_index);
goto done;
failed:
platform_log("menus: not added; using the game's own menus");
platform_log(in_game ? "menus: not added; no SETTINGS in the pause menu" :
"menus: not added; using the game's own menus");
menu_tags_release();
done:
@@ -1360,6 +1485,9 @@ done:
free(build.spinner_tags);
free(build.strings_tags);
free(build.bitmap_tags);
free(build.wanted_widgets);
free(build.wanted_bitmaps);
free(build.wanted_strings);
memset(&build, 0, sizeof(build));
}
@@ -1377,7 +1505,7 @@ char const *pc_menus_root_name(
settings' screens) */
char const *open = config_string("debug.menu_open");
if (menu_tags.loaded && *open && build.menus == NULL)
if (menu_tags.loaded && !menu_tags.in_game && *open && build.menus == NULL)
{
static char name[300];
long index;
@@ -1442,7 +1570,7 @@ char const *pc_menus_screen(
};
short index;
if (!menu_tags.loaded)
if (!menu_tags.loaded || menu_tags.in_game)
return name;
for (index = 0; index < NUMBEROF(screens); index++)
{
+157
View File
@@ -3694,6 +3694,16 @@ static void widget_instance_initialize(
widget->render_regardless_of_controller_index =
TEST_FLAG(definition->flags, _widget_render_regardless_of_controller_index_bit);
widget->pause_game_time = TEST_FLAG(definition->flags, _widget_pause_game_time_bit);
/* port: the menus' screens (port/assets/menus) pause the game, as its
pause menu does, but not a network game, which its pause menu does not
pause (their SETTINGS: ui_pause_menu_loaded) */
if (widget->pause_game_time &&
pc_menu_tag(tag_index) &&
!we_are_at_the_main_menu &&
network_game_is_active())
{
widget->pause_game_time = FALSE;
}
widget->creation_time = widget_globals.current_system_milliseconds;
widget->milliseconds_to_auto_close = MAX(definition->milliseconds_to_auto_close, 0);
widget->auto_close_fade_time = MAX(definition->auto_close_fade_time, 0);
@@ -3831,6 +3841,144 @@ void ui_widget_port_go_back(
widget_instance_go_back_to_previous(widget);
}
/* ---------- SETTINGS in the pause menus (port)
The full screen pause menus (the campaign's, and a network game's with one
player here) get SETTINGS after RESUME GAME: the menus' own row for it
(port/assets/menus/ce/in_game.xml), which menu_tags.c builds in the game's
maps when the menus are there. It opens its screen as any widget opens
another, so B comes back here, to SETTINGS. The rows are found by their tags'
names, not their places, and spaced to fit the box (ui_pause_menu_rows_fit):
a row added after SETTINGS is spaced with the others. */
#define UI_PAUSE_MENU_SETTINGS_TAG "pc\\in_game\\settings_button"
enum
{
/* the body of the pause menus' box between the lines of its art
(ui\shell\bitmaps\pausebox2_*), and how far inside it the rows stay */
UI_PAUSE_BOX_BODY_TOP = 2,
UI_PAUSE_BOX_BODY_BOTTOM = 130,
UI_PAUSE_BOX_ROWS_MARGIN = 4
};
/* whether the widget is drawn from a tag whose name ends with the leaf */
static boolean ui_pause_menu_tag_is(
struct widget_instance *widget,
char const *leaf)
{
char const *name = tag_get_name(widget->definition_tag_index);
char const *last = name ? strrchr(name, '\\') : NULL;
return last && !csstrcmp(last + 1, leaf);
}
/* the rows spaced as the map spaces them, around the middle where it has
them, if they fit in the box's body; else the widest spacing that fits */
static void ui_pause_menu_rows_fit(
struct widget_instance *list,
struct widget_instance *box)
{
struct ui_widget_definition *definition = ui_widget_definition_get(list->definition_tag_index);
struct ui_widget_child_reference *references =
(struct ui_widget_child_reference *)definition->child_widgets.address;
struct ui_widget_definition *row_definition;
struct widget_instance *row;
short row_count = 0;
short row_height, pitch, middle_twice, body_top, body_bottom, span, top;
for (row = list->child; row; row = row->next)
row_count++;
if (definition->child_widgets.count < 2 || row_count < 2)
return;
row_definition = ui_widget_definition_get(list->child->definition_tag_index);
row_height = row_definition->bounds.y1 - row_definition->bounds.y0;
pitch = references[1].vertical_offset - references[0].vertical_offset;
/* (on the screen, as the list's place and the box's are) */
middle_twice = 2 * list->vertical_offset + references[0].vertical_offset +
references[definition->child_widgets.count - 1].vertical_offset + row_height;
body_top = box->vertical_offset + UI_PAUSE_BOX_BODY_TOP + UI_PAUSE_BOX_ROWS_MARGIN;
body_bottom = box->vertical_offset + UI_PAUSE_BOX_BODY_BOTTOM - UI_PAUSE_BOX_ROWS_MARGIN;
if ((row_count - 1) * pitch + row_height > body_bottom - body_top)
pitch = (body_bottom - body_top - row_height) / (row_count - 1);
span = (row_count - 1) * pitch + row_height;
top = PIN((middle_twice - span) / 2, body_top, body_bottom - span);
row_count = 0;
for (row = list->child; row; row = row->next)
row->vertical_offset = top - list->vertical_offset + row_count++ * pitch;
return;
}
static void ui_pause_menu_loaded(
struct widget_instance *root)
{
static char const *const pause_menus[] =
{
"ui\\shell\\solo_game\\pause_game\\pause_game",
"ui\\shell\\multiplayer_game\\pause_game\\1p_pause_game",
};
char const *name = tag_get_name(root->definition_tag_index);
long settings_tag_index;
struct widget_instance *box = NULL;
struct widget_instance *list = NULL;
struct widget_instance *resume = NULL;
struct widget_instance *child;
struct widget_instance *settings;
short index;
for (index = 0; name && index < (short)NUMBEROF(pause_menus); index++)
{
if (!csstrcmp(name, pause_menus[index]))
break;
}
/* (and with one player here: the campaign's falls back on its full
screen pause menu for more, ui_check_for_pause_game) */
if (!name || index == (short)NUMBEROF(pause_menus) || local_player_count() > 1)
return;
settings_tag_index = tag_loaded(UI_WIDGET_DEFINITION_TAG, UI_PAUSE_MENU_SETTINGS_TAG);
if (settings_tag_index == NONE)
return;
for (child = root->child; child; child = child->next)
{
if (ui_pause_menu_tag_is(child, "pause_dialog_bkd"))
{
box = child;
}
else if (child->type == _ui_widget_type_column_list && !resume)
{
for (resume = child->child; resume; resume = resume->next)
{
if (ui_pause_menu_tag_is(resume, "resume_game_button"))
break;
}
if (resume)
list = child;
}
}
if (!box || !list)
return;
settings = ui_widget_load_by_name_or_tag(
NULL,
settings_tag_index,
list,
list->local_player_index,
NONE,
NONE,
NONE);
if (!settings)
return;
settings->horizontal_offset = resume->horizontal_offset;
settings->previous = resume;
settings->next = resume->next;
if (resume->next)
resume->next->previous = settings;
resume->next = settings;
ui_pause_menu_rows_fit(list, box);
return;
}
struct widget_instance *ui_widget_load_by_name_or_tag(
char const *name,
long tag_index,
@@ -3925,6 +4073,10 @@ struct widget_instance *ui_widget_load_by_name_or_tag(
tag_index,
local_player_index,
widget_stack);
/* port: SETTINGS in the pause menus, unless a created handler
closed the widget */
if (!parent && widget_globals.active_widgets[widget_stack] == widget)
ui_pause_menu_loaded(widget);
}
else
{
@@ -7180,6 +7332,11 @@ static boolean ui_check_for_pause_game(
else
{
ui_widget_delete(widget_globals.active_widgets[controller_index]);
/* port: and the screens B would have gone back to (the
pause menu, from its SETTINGS), as the campaign's START
closes them all (ui_widgets_close_all) */
if (widget_globals.widget_stack[controller_index])
dispose_widget_stack(&widget_globals.widget_stack[controller_index]);
}
}
}
+2 -1
View File
@@ -567,7 +567,8 @@ def main() -> None:
lines += widget_xml(tag, widgets[tag], tags, functions, inputs, fonts, {})
lines.append("</menus>")
(CE / f"{folder.replace('/', '.')}.xml").write_text("\n".join(lines) + "\n")
for name, lines in [*port_settings.settings_files().items(), *port_settings.multiplayer_files().items()]:
for name, lines in [*port_settings.settings_files().items(), *port_settings.multiplayer_files().items(),
*port_settings.in_game_files().items()]:
(CE / name).write_text("\n".join(lines))
# their strings
lines = ['<?xml version="1.0" encoding="UTF-8"?>',
+89
View File
@@ -745,3 +745,92 @@ def multiplayer_files() -> dict:
REPLACED_FOLDERS = [f"{MT}/server_settings"]
# ---------- in a game: SETTINGS in the pause menus
# The game's pause menus are the maps' own, not these files', and ui.map has
# none of their art, so these widgets (in_game/...) are built in the game's
# maps instead, with the screens they open (port/linux/game/menu_tags.c);
# ui_widget.c adds SETTINGS to the pause menus after RESUME GAME. Its screen
# is the multiplayer pause menu's box and key, from the maps' own tags (every
# campaign and multiplayer map has them), with rows as the pause menus have
# them, named and ordered as Edit Profile has them. Each opens Edit Profile's
# screen over the pause menu's dim (behind them, the main menu has its dark
# backdrop, a game the game), and the screen saves config.toml's settings as
# it does there; GAMEPADS, on OK, only the controller settings of the
# player's active profile ("port active profile edit begin" and "end",
# port/linux/game/menu_functions.c).
# The desktop builds' only, for now.
IG = "in_game"
PAUSE_DIM = "ui\\shell\\bitmaps\\semi_transparent_grey"
PAUSE_ROW = [("type", "text"), ("width", 202), ("height", 27), ("bitmap", "ui\\shell\\bitmaps\\menu_bkds"),
("font", "ui\\large_ui"), ("color", "#FF2896FF"), ("align", "center"), ("text_y", 3)]
DESKTOP = [("platform", "desktop")]
# Edit Profile's setups: their rows' names (its profile_edit_options), and
# the screens they open
IN_GAME_SETUPS = [("controls_setup", 1, f"{PE}/controls_setup/controls_settings_screen"),
("gamepads", 2, f"{PE}/gamepad_setup/gamepad_setup_screen"),
("mouse_setup", 3, f"{PE}/mouse_settings/mouse_settings_screen"),
("audio_setup", 4, f"{PE}/audio_settings/audio_settings_screen"),
("video_setup", 5, f"{PE}/video_settings/video_settings_screen")]
# the box where the multiplayer pause menu has it; the body of its art
# (pausebox2_*) between its rows 2 and 130, the key's band below; the rows
# that far inside it (ui_widget.c's UI_PAUSE_BOX_ROWS_MARGIN), spaced as the
# multiplayer pause menu's at most
BOX_Y, BOX_BODY_TOP, BOX_BODY_BOTTOM, ROWS_MARGIN, ROW_HEIGHT, ROW_SPACING = 164, 2, 130, 4, 27, 35
def _pause_row(name: str, label: list, handlers: list) -> list:
return _widget(name, label + PAUSE_ROW + DESKTOP, handlers)
def _opens(screen: str, run: str = None) -> list:
action = (f'run="{run}" ' if run else "") + f'open="{screen}"'
return [f'<on event="a" {action}/>', f'<on event="start" {action}/>']
def _rows_place(count: int) -> tuple:
"""the rows' top and spacing: in the middle of the box's body"""
body = BOX_BODY_BOTTOM - BOX_BODY_TOP
spacing = min(ROW_SPACING, (body - 2 * ROWS_MARGIN - ROW_HEIGHT) // (count - 1))
span = (count - 1) * spacing + ROW_HEIGHT
return BOX_Y + BOX_BODY_TOP + (body - span) // 2, spacing
def in_game_files() -> dict:
"""SETTINGS in the pause menus, its screen, and the setups' screens"""
top, spacing = _rows_place(len(IN_GAME_SETUPS))
lines = _pause_row(f"{IG}/settings_button", [("text", "SETTINGS")], _opens(f"{IG}/settings_screen"))
lines += _widget(f"{IG}/settings_screen",
[("width", 640), ("height", 480), ("flags", "pass_unhandled_to_focused_child pause_game"),
("bitmap", PAUSE_DIM)] + DESKTOP,
['<on event="b" back="true"/>', '<on event="back" back="true"/>',
f'<child widget="{IG}/settings_box" x="210" y="{BOX_Y}"/>',
f'<child widget="{IG}/settings_list" x="218" y="{top}"/>',
'<child widget="ui\\shell\\main_menu\\button_key_sm" x="235" y="298"/>'])
lines += _widget(f"{IG}/settings_box", [("width", 226), ("height", 154)] + DESKTOP,
['<child widget="ui\\shell\\solo_game\\pause_game\\pausebox2_left" x="-4"/>',
'<child widget="ui\\shell\\solo_game\\pause_game\\pausebox2_left_center" x="12"/>',
'<child widget="ui\\shell\\solo_game\\pause_game\\pausebox2_right" x="206"/>'])
lines += _widget(f"{IG}/settings_list",
[("type", "column_list"), ("width", 202),
("height", (len(IN_GAME_SETUPS) - 1) * spacing + ROW_HEIGHT),
("flags", "pass_unhandled_to_focused_child up_down_tabs_items")] + DESKTOP,
[f'<child widget="{IG}/{key}_button" y="{index * spacing}"/>'
for index, (key, _, _) in enumerate(IN_GAME_SETUPS)])
for key, string_index, screen in IN_GAME_SETUPS:
# (GAMEPADS sets the profile's controller settings: the player's
# active profile, being edited while its screen is up)
gamepads = key == "gamepads"
lines += _pause_row(f"{IG}/{key}_button",
[("string_list", f"{PE}/profile_edit_options"), ("string_index", string_index)],
_opens(f"{IG}/{key}_screen", "port active profile edit begin" if gamepads else None))
lines += _widget(f"{IG}/{key}_screen",
[("width", 640), ("height", 480), ("flags", "pass_unhandled_to_focused_child"),
("bitmap", PAUSE_DIM)] + DESKTOP,
(['<on event="deleted" run="port active profile edit end"/>'] if gamepads else []) +
[f'<child widget="{screen}"/>'])
return {"in_game.xml": ['<?xml version="1.0" encoding="UTF-8"?>',
"<!-- The port's SETTINGS in the game's pause menus (tools/port_settings.py) -->",
"<menus>", *lines, "</menus>", ""]}