diff --git a/port/assets/menus/README.md b/port/assets/menus/README.md
index c8ea5779..68fc64cd 100644
--- a/port/assets/menus/README.md
+++ b/port/assets/menus/README.md
@@ -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 ``
diff --git a/port/assets/menus/ce/in_game.xml b/port/assets/menus/ce/in_game.xml
new file mode 100644
index 00000000..ed64de4f
--- /dev/null
+++ b/port/assets/menus/ce/in_game.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/port/assets/menus/menus.json b/port/assets/menus/menus.json
index 6dc6e4e3..acbbd308 100644
--- a/port/assets/menus/menus.json
+++ b/port/assets/menus/menus.json
@@ -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",
diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c
index e6388323..36d4b9fb 100644
--- a/port/linux/game/menu_functions.c
+++ b/port/linux/game/menu_functions.c
@@ -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);
diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c
index 74a4a29e..33414605 100644
--- a/port/linux/game/menu_tags.c
+++ b/port/linux/game/menu_tags.c
@@ -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++)
{
diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c
index f5502acb..cf52aeb2 100644
--- a/source/interface/ui_widget.c
+++ b/source/interface/ui_widget.c
@@ -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]);
}
}
}
diff --git a/tools/ce_menus.py b/tools/ce_menus.py
index 728bb111..a31a6337 100644
--- a/tools/ce_menus.py
+++ b/tools/ce_menus.py
@@ -567,7 +567,8 @@ def main() -> None:
lines += widget_xml(tag, widgets[tag], tags, functions, inputs, fonts, {})
lines.append("")
(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 = ['',
diff --git a/tools/port_settings.py b/tools/port_settings.py
index 1068b120..bb1ed699 100644
--- a/tools/port_settings.py
+++ b/tools/port_settings.py
@@ -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'', f'']
+
+
+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,
+ ['', '',
+ f'',
+ f'',
+ ''])
+ lines += _widget(f"{IG}/settings_box", [("width", 226), ("height", 154)] + DESKTOP,
+ ['',
+ '',
+ ''])
+ 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''
+ 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,
+ ([''] if gamepads else []) +
+ [f''])
+ return {"in_game.xml": ['',
+ "",
+ "", *lines, "", ""]}