mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-17 12:00:26 +02:00
Tell a joining player which map they are missing
A client joining a game on a map it does not have (a Custom Edition map not in custom_maps, or one of the game's own not in maps) now leaves with an error naming the map and where to copy it, instead of the damaged disc error. It also says when the map cannot run there or the host names a Custom Edition map as other versions do. The port's error texts wrap to their dialog, in the smaller font when they would not fit. Network version 22: hosts now name Custom Edition maps custom_maps\<name>, which older clients cannot load or explain; they are told to update.
This commit is contained in:
1 parent
ce41b41d78
commit
4e8ed2f196
9 files changed
+287
-2
No files matched your search
@@ -68,6 +68,18 @@ levels have:
|
||||
- `<name>.txt`, its description: plain text, its lines shown as written.
|
||||
Without one, the map is described as "Halo Custom Edition map".
|
||||
|
||||
A machine joining a network game on a map it has not (a Custom Edition map
|
||||
not in its `custom_maps`, or one of the game's own not in `maps`) leaves it
|
||||
with an error that names the map and the folder to copy it into, rather
|
||||
than the damaged disc error precaching a missing map gives
|
||||
(`cache_files_map_present`, called as the host's settings arrive): also when
|
||||
the map cannot run there (Custom Edition maps turned off, the file not a
|
||||
Custom Edition cache, or `bitmaps.map`, `sounds.map` and `loc.map` missing),
|
||||
and when the host names a Custom Edition map as other versions of this port
|
||||
do (`levels\test\<name>\<name>`) and the file is in `custom_maps`. The
|
||||
error's text is wrapped to its dialog, in the menus' smaller font when it
|
||||
would not fit; `debug.txt` has the details.
|
||||
|
||||
A Custom Edition campaign map has no next level: winning it ends the game
|
||||
as the campaign's last level does (alone) or plays it again (network
|
||||
co-op), and it is not the campaign's saved game.
|
||||
@@ -421,7 +433,9 @@ menus (on a virtual display): New Game's CUSTOM SINGLEPLAYER `a30` played as
|
||||
a campaign level at the difficulty chosen; the Map screen's (LAN) listed its
|
||||
difficulties, set up co-op in Server Setup and showed it in the lobby as
|
||||
co-op. With `debug.network_test`, two machines on loopback played
|
||||
`custom_maps\a30` as network co-op and `custom_maps\hugeass` as slayer.
|
||||
`custom_maps\a30` as network co-op and `custom_maps\hugeass` as slayer; a
|
||||
joining machine without the map, without the resource maps, or with a maps
|
||||
folder missing the host's Xbox map was shown each error and left the game.
|
||||
|
||||
### Not tested
|
||||
|
||||
|
||||
@@ -566,6 +566,78 @@ boolean custom_edition_level_name(
|
||||
!_strnicmp(level_name, CUSTOM_EDITION_LEVEL_NAME_PREFIX, csstrlen(CUSTOM_EDITION_LEVEL_NAME_PREFIX));
|
||||
}
|
||||
|
||||
boolean custom_edition_map_file_present(
|
||||
char const *map_name)
|
||||
{
|
||||
char path[MAP_PATH_SIZE];
|
||||
|
||||
return custom_edition_map_path(map_name, path);
|
||||
}
|
||||
|
||||
boolean custom_edition_cache_present(
|
||||
char const *level_name,
|
||||
char *message,
|
||||
long message_size)
|
||||
{
|
||||
char const *name = tag_name_strip_path(level_name);
|
||||
char path[MAP_PATH_SIZE];
|
||||
struct custom_edition_file file;
|
||||
struct cache_file_identity identity;
|
||||
enum cache_file_status status = _cache_file_status_read_failed;
|
||||
short folder;
|
||||
short type;
|
||||
|
||||
if (!custom_edition_map_path(level_name, path))
|
||||
{
|
||||
snprintf(message, message_size, "You don't have the map %.64s.map. If you have it, copy it into custom_maps.",
|
||||
name);
|
||||
return FALSE;
|
||||
}
|
||||
if (!halo_custom_edition_tag_cache())
|
||||
{
|
||||
error(_error_silent, "custom edition: the map '%s' cannot be played: %s", level_name,
|
||||
custom_edition_unavailable_reason());
|
||||
snprintf(message, message_size, "The map %.64s can't be played: Custom Edition maps can't run (see debug.txt).",
|
||||
name);
|
||||
return FALSE;
|
||||
}
|
||||
if (custom_edition_file_open(&file, path))
|
||||
{
|
||||
status = cache_file_identify(&file.source, &identity);
|
||||
custom_edition_file_close(&file);
|
||||
}
|
||||
if (status != _cache_file_status_ok || identity.format != _cache_file_format_custom_edition_cache)
|
||||
{
|
||||
error(_error_silent, "custom edition: '%s' cannot be played: %s", path,
|
||||
status != _cache_file_status_ok ? cache_file_status_describe(status) :
|
||||
"it is not a Halo Custom Edition cache");
|
||||
snprintf(message, message_size, "Your %.64s.map can't be played (debug.txt says why).", name);
|
||||
return FALSE;
|
||||
}
|
||||
/* (the resource maps every Custom Edition map's tags are read from) */
|
||||
for (type = _resource_map_bitmaps; type < NUMBER_OF_RESOURCE_MAP_TYPES; type++)
|
||||
{
|
||||
char const *resource_name = resource_map_type_describe((enum resource_map_type)type);
|
||||
char resource_path[MAP_PATH_SIZE];
|
||||
|
||||
for (folder = 0; folder < NUMBER_OF_MAPS_FOLDERS; folder++)
|
||||
{
|
||||
if (maps_folder_has(maps_folder(folder), resource_name, MAP_FILE_EXTENSION, resource_path))
|
||||
break;
|
||||
}
|
||||
if (folder == NUMBER_OF_MAPS_FOLDERS)
|
||||
{
|
||||
error(_error_silent, "custom edition: the map '%s' needs %s.map, which no maps folder has", level_name,
|
||||
resource_name);
|
||||
snprintf(message, message_size,
|
||||
"The map %.64s needs Custom Edition's bitmaps.map, sounds.map and loc.map in custom_maps.", name);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
boolean custom_edition_cache_playable(
|
||||
char const *level_name)
|
||||
{
|
||||
|
||||
@@ -76,6 +76,20 @@ boolean custom_edition_level_name(
|
||||
it is then read in place, never copied to the cache partition. */
|
||||
boolean custom_edition_cache_playable(
|
||||
char const *level_name);
|
||||
/* Whether the map `map_name` (a level name or a file name) has a file in the
|
||||
Custom Edition maps folders, whatever it holds. */
|
||||
boolean custom_edition_map_file_present(
|
||||
char const *map_name);
|
||||
/* Whether this machine can play the level `level_name` (custom_maps\<name>),
|
||||
as a network game's client must: its file present and a Custom Edition
|
||||
cache, Custom Edition maps able to run, and the resource maps present. When
|
||||
not, `message` (`message_size` characters) says why, for the player, and
|
||||
what to do. */
|
||||
boolean custom_edition_cache_present(
|
||||
char const *level_name,
|
||||
char *message,
|
||||
long message_size);
|
||||
|
||||
/* Whether the file `file_name` (custom_maps\<file_name>.map) is a Custom
|
||||
Edition cache of a campaign map (a solo scenario, played alone or as
|
||||
network co-op), or of a multiplayer map (custom_edition_maps.c). */
|
||||
|
||||
@@ -66,7 +66,7 @@ is newer (network_client_manager.c). A host advertises it, with its netcode,
|
||||
in its game's advertisement's reserved bytes (network_server_message_handler.c),
|
||||
which hosts built before there was a version send as zeros: version 0.
|
||||
Raise it with any change to what the machines send each other. */
|
||||
#define HALO_PORT_NETWORK_VERSION 21
|
||||
#define HALO_PORT_NETWORK_VERSION 22
|
||||
/* ... the advertisement's reserved bytes: the version (a little-endian word),
|
||||
then flags */
|
||||
#define HALO_PORT_ADVERTISED_VERSION_OFFSET 0
|
||||
|
||||
Vendored
+60
@@ -1035,6 +1035,66 @@ void cache_files_show_multiplayer_unavailable(
|
||||
return;
|
||||
}
|
||||
|
||||
/* port: whether this machine has the map a network game is on (a client
|
||||
joining it: network_client_manager.c); when not, tells the player which map
|
||||
is missing and where to copy it, in the error the main menu shows next,
|
||||
rather than the damaged disc error that precaching a map that is not there
|
||||
gives (cache_files_give_time_to_precache).
|
||||
A Halo Custom Edition map (custom_maps\<name>) is looked for in the Custom
|
||||
Edition maps folders (port/linux/game/custom_edition_cache.c), any other in
|
||||
the game's own. */
|
||||
boolean cache_files_map_present(
|
||||
char const *map_name)
|
||||
{
|
||||
void platform_log(char const *format, ...);
|
||||
wchar_t error_text[512];
|
||||
char const *name = tag_name_strip_path(map_name);
|
||||
char message[512];
|
||||
short index;
|
||||
|
||||
if (!map_name || !map_name[0])
|
||||
return TRUE;
|
||||
if (custom_edition_level_name(map_name))
|
||||
{
|
||||
if (custom_edition_cache_present(map_name, message, sizeof(message)))
|
||||
return TRUE;
|
||||
}
|
||||
else
|
||||
{
|
||||
char path[256];
|
||||
HANDLE file;
|
||||
|
||||
if (cache_files_precache_map_loaded(map_name))
|
||||
return TRUE;
|
||||
snprintf(path, sizeof(path), "%s%s.map", cache_files_map_directory(), name);
|
||||
file = CreateFileA(path, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
|
||||
if (file != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
CloseHandle(file);
|
||||
return TRUE;
|
||||
}
|
||||
/* (a host of another version of this port, which names a Custom
|
||||
Edition map as the game's own maps are named) */
|
||||
if (custom_edition_map_file_present(name))
|
||||
{
|
||||
snprintf(message, sizeof(message),
|
||||
"The host's map %.64s is a Custom Edition map named for another version of this game.", name);
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(message, sizeof(message), "You don't have the map %.64s.map. If you have it, copy it into maps.",
|
||||
name);
|
||||
}
|
||||
}
|
||||
platform_log("map missing: %s", message);
|
||||
for (index = 0; message[index] && index < NUMBEROF(error_text) - 1; index++)
|
||||
error_text[index] = (wchar_t)(unsigned char)message[index];
|
||||
error_text[index] = 0;
|
||||
display_error_text_when_main_menu_loaded(error_text);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
boolean cache_files_give_time_to_precache(
|
||||
char const *map_name)
|
||||
{
|
||||
|
||||
Vendored
+2
@@ -73,6 +73,8 @@ boolean cache_files_map_plays_multiplayer(
|
||||
void cache_files_show_multiplayer_unavailable(
|
||||
char const *map_name,
|
||||
char const *build);
|
||||
boolean cache_files_map_present(
|
||||
char const *map_name);
|
||||
|
||||
unsigned long cache_files_get_checksum(
|
||||
void);
|
||||
|
||||
@@ -1968,6 +1968,29 @@ void display_error_when_main_menu_loaded(
|
||||
return;
|
||||
}
|
||||
|
||||
/* port: an error of the port's own text (as display_error_text_deferred's)
|
||||
shown when the main menu is next loaded, as display_error_when_main_menu_loaded
|
||||
shows one of the game's: a network game left for a reason the game has no
|
||||
error for (a map missing: cache_files.c). The text is copied, when no error
|
||||
is queued already. */
|
||||
void display_error_text_when_main_menu_loaded(
|
||||
wchar_t const *text)
|
||||
{
|
||||
static wchar_t queued_text[512];
|
||||
|
||||
if (widget_globals.main_menu_deferred_error_code != NONE)
|
||||
{
|
||||
error(_error_silent, "there is already an error message queued for display at the main menu; ignoring this one");
|
||||
return;
|
||||
}
|
||||
ustrncpy(queued_text, text, NUMBEROF(queued_text) - 1);
|
||||
queued_text[NUMBEROF(queued_text) - 1] = 0;
|
||||
ui_widget_port_error_pending_text = queued_text;
|
||||
widget_globals.main_menu_deferred_error_code = _error_cannot_create_saved_game_file_with_empty_name;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void display_error_abort_to_dashboard_deferred(
|
||||
short error_code,
|
||||
boolean optional)
|
||||
@@ -5113,6 +5136,61 @@ static long search_and_replace(
|
||||
return replacements;
|
||||
}
|
||||
|
||||
/* port: the port's own error text (display_error_text_deferred), which may be
|
||||
longer than a line of its dialog: broken at spaces (the strings' line break,
|
||||
'\r') where a line would be wider than `width` in the font drawn with.
|
||||
FALSE when a line is still wider (a word longer than a line). */
|
||||
static boolean ui_widget_port_text_wrap(
|
||||
wchar_t *text,
|
||||
rectangle2d const *bounds,
|
||||
short width)
|
||||
{
|
||||
long line_start = 0;
|
||||
long last_space = NONE;
|
||||
long index;
|
||||
boolean fits = TRUE;
|
||||
|
||||
for (index = 0; ; index++)
|
||||
{
|
||||
wchar_t character = text[index];
|
||||
|
||||
if (character == L'\r' || character == L'\n')
|
||||
{
|
||||
line_start = index + 1;
|
||||
last_space = NONE;
|
||||
continue;
|
||||
}
|
||||
if (character == L' ' || character == 0)
|
||||
{
|
||||
wchar_t line[256];
|
||||
long length = MIN(index - line_start, (long)NUMBEROF(line) - 1);
|
||||
rectangle2d text_bounds;
|
||||
rectangle2d cursor_bounds;
|
||||
|
||||
csmemcpy(line, text + line_start, length * sizeof(wchar_t));
|
||||
line[length] = 0;
|
||||
draw_unicode_string_compute_bounds(bounds, line, &text_bounds, &cursor_bounds);
|
||||
if (cursor_bounds.x0 - bounds->x0 > width && last_space != NONE)
|
||||
{
|
||||
text[last_space] = L'\r';
|
||||
line_start = last_space + 1;
|
||||
/* (the line begun, the word just measured: wider alone?) */
|
||||
length = MIN(index - line_start, (long)NUMBEROF(line) - 1);
|
||||
csmemcpy(line, text + line_start, length * sizeof(wchar_t));
|
||||
line[length] = 0;
|
||||
draw_unicode_string_compute_bounds(bounds, line, &text_bounds, &cursor_bounds);
|
||||
}
|
||||
if (cursor_bounds.x0 - bounds->x0 > width)
|
||||
fits = FALSE;
|
||||
if (character == 0)
|
||||
break;
|
||||
last_space = index;
|
||||
}
|
||||
}
|
||||
|
||||
return fits;
|
||||
}
|
||||
|
||||
static void widget_instance_render_text_box(
|
||||
struct widget_instance *widget,
|
||||
struct ui_widget_definition *definition,
|
||||
@@ -5249,6 +5327,41 @@ static void widget_instance_render_text_box(
|
||||
SECONDS_PER_MILLISECOND * 3.0f) + 1.5f) * 0.4f) * color.alpha;
|
||||
}
|
||||
draw_string_set_draw_mode(font_index, NONE, justification, 0, &color);
|
||||
/* port: the port's own error text, wrapped to its dialog: the box its
|
||||
background draws (narrower than the text box; its picture is padded to a
|
||||
power of two, so a margin of the text's offset on each side and as much
|
||||
again), and as tall as the text box less its offset above and as much
|
||||
below (the dialog's footer). Text that would be taller, or wider (a word
|
||||
longer than a line), is drawn in the menus' smaller font. */
|
||||
if (widget == ui_widget_port_error_text_box && ui_widget_port_error_text)
|
||||
{
|
||||
struct bitmap_data *background = definition->background_bitmap.index != NONE ?
|
||||
bitmap_group_get_bitmap_from_sequence(definition->background_bitmap.index, 0, 0) : NULL;
|
||||
short width = (short)(definition->bounds.x1 - definition->bounds.x0);
|
||||
short height = (short)(definition->bounds.y1 - definition->bounds.y0 - 2 * definition->vertical_offset);
|
||||
wchar_t wrapped[512];
|
||||
rectangle2d text_bounds;
|
||||
rectangle2d cursor_bounds;
|
||||
|
||||
if (background && background->width > 0 && background->width < width)
|
||||
width = background->width;
|
||||
width = (short)(width - 4 * definition->horizontal_offset);
|
||||
ustrncpy(wrapped, *text, NUMBEROF(wrapped) - 1);
|
||||
wrapped[NUMBEROF(wrapped) - 1] = 0;
|
||||
if (!ui_widget_port_text_wrap(wrapped, &bounds, width) ||
|
||||
(draw_unicode_string_compute_bounds(&bounds, wrapped, &text_bounds, &cursor_bounds),
|
||||
cursor_bounds.y1 - bounds.y0 > height))
|
||||
{
|
||||
long small_font_index = tag_loaded('font', "ui\\small_ui");
|
||||
|
||||
if (small_font_index != NONE)
|
||||
{
|
||||
font_index = small_font_index;
|
||||
draw_string_set_draw_mode(font_index, NONE, justification, 0, &color);
|
||||
}
|
||||
}
|
||||
ui_widget_port_text_wrap(*text, &bounds, width);
|
||||
}
|
||||
if (string_has_icons_to_draw(*text))
|
||||
draw_string_and_hack_in_icons(&bounds, &clip, NULL, 0, *text, FALSE);
|
||||
else
|
||||
|
||||
@@ -137,6 +137,8 @@ void display_error_deferred(
|
||||
void display_error_text_deferred(
|
||||
wchar_t const *text,
|
||||
short local_player_index);
|
||||
void display_error_text_when_main_menu_loaded(
|
||||
wchar_t const *text);
|
||||
void display_error_abort_to_dashboard_deferred(
|
||||
short error_code,
|
||||
boolean optional);
|
||||
|
||||
@@ -1287,6 +1287,14 @@ boolean network_game_client_game_settings_updated(
|
||||
{
|
||||
char build[0x20];
|
||||
|
||||
/* port: a map this machine has not (a Custom Edition map not in
|
||||
its custom_maps folder, say): the player told which and where to
|
||||
copy it (the main menu's error, in place of the failed join's), and
|
||||
the game left, rather than precaching it, which would give the
|
||||
damaged disc error (cache_files.c) */
|
||||
if (!network_game_is_splitscreen_local() && !cache_files_map_present(message_packet->map.name))
|
||||
return FALSE;
|
||||
|
||||
/* port: a map of a build this version does not play with others
|
||||
(its objects would not be the host's): said, and the game left */
|
||||
if (!network_game_is_splitscreen_local() &&
|
||||
|
||||
Reference in new issue
Block a user