Add high-resolution UI controller buttons

This commit is contained in:
saulo benigno committed 2026-10-02 21:10:04 -03:00
1 parent d1c7243cb2
commit c72962a73d
20 files changed
+843 -21

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+172
View File
@@ -0,0 +1,172 @@
{
"assets": [
{
"name": "a_butn__0",
"tag": "ui\\shell\\bitmaps\\a_butn",
"bitmap": 0,
"width": 32,
"height": 32,
"format": "dxt3",
"scale": 8,
"crc": 1140253829,
"svg": "a_butn.svg",
"place": [
1,
0,
0
]
},
{
"name": "b_butn__0",
"tag": "ui\\shell\\bitmaps\\b_butn",
"bitmap": 0,
"width": 32,
"height": 32,
"format": "dxt3",
"scale": 8,
"crc": 2900806204,
"svg": "b_butn.svg",
"place": [
1,
0,
0
]
},
{
"name": "x_butn__0",
"tag": "ui\\shell\\bitmaps\\x_butn",
"bitmap": 0,
"width": 32,
"height": 32,
"format": "dxt3",
"scale": 8,
"crc": 2247463469,
"svg": "x_butn.svg",
"place": [
1,
0,
0
]
},
{
"name": "y_butn__0",
"tag": "ui\\shell\\bitmaps\\y_butn",
"bitmap": 0,
"width": 32,
"height": 32,
"format": "dxt3",
"scale": 8,
"crc": 783963872,
"svg": "y_butn.svg",
"place": [
1,
0,
0
]
},
{
"name": "a_butn_sm__0",
"tag": "ui\\shell\\bitmaps\\a_butn_sm",
"bitmap": 0,
"width": 32,
"height": 32,
"format": "a8r8g8b8",
"scale": 8,
"crc": 2356213272,
"svg": "a_butn.svg",
"place": [
0.7702,
-0.222,
-0.272
]
},
{
"name": "b_butn_sm__0",
"tag": "ui\\shell\\bitmaps\\b_butn_sm",
"bitmap": 0,
"width": 32,
"height": 32,
"format": "a8r8g8b8",
"scale": 8,
"crc": 3999851658,
"svg": "b_butn.svg",
"place": [
0.7677,
-0.14,
-0.225
]
},
{
"name": "hud_msg_icons_sm__0",
"tag": "ui\\hud\\bitmaps\\hud_msg_icons_sm",
"bitmap": 0,
"width": 128,
"height": 128,
"format": "a8y8",
"scale": 8,
"crc": 2908635490,
"sprites": [
{
"sequence": 0,
"cell": [
68,
21,
84,
41
],
"svg": "a_butn.svg",
"place": [
0.7069,
1.042,
1.022
]
},
{
"sequence": 1,
"cell": [
103,
0,
119,
20
],
"svg": "b_butn.svg",
"place": [
0.6835,
1.652,
1.538
]
},
{
"sequence": 2,
"cell": [
71,
0,
87,
20
],
"svg": "x_butn.svg",
"place": [
0.7016,
0.98,
1.244
]
},
{
"sequence": 3,
"cell": [
87,
0,
103,
20
],
"svg": "y_butn.svg",
"place": [
0.6985,
1.09,
1.277
]
}
]
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<!-- The A button of the menus' button keys (ui\shell\bitmaps\a_butn), redrawn: drawn
in the 32x32 bitmap's texels, where its art is (tools/button_assets.py). A tilted
green oval, dark at the top and bright at the bottom, with a pale rim, and its
letter in white, upright from its baseline and slanted. -->
<defs>
<!-- (along the oval's long axis, top to bottom) -->
<linearGradient id="body" gradientUnits="userSpaceOnUse" x1="0" y1="-12.932" x2="0" y2="12.932">
<stop offset="0" stop-color="#08370f"/>
<stop offset="0.5" stop-color="#056a1c"/>
<stop offset="1" stop-color="#0a9924"/>
</linearGradient>
<linearGradient id="rim" gradientUnits="userSpaceOnUse" x1="0" y1="-12.932" x2="0" y2="12.932">
<stop offset="0" stop-color="#ffffff" stop-opacity="0.74"/>
<stop offset="1" stop-color="#ffffff" stop-opacity="0.73"/>
</linearGradient>
</defs>
<g transform="translate(10.341 12.879) rotate(-26.459)">
<ellipse id="oval" rx="8.659" ry="12.932" fill="url(#body)"/>
<ellipse rx="8.082" ry="12.355" fill="none" stroke="url(#rim)" stroke-width="1.154"/>
</g>
<path id="letter" transform="matrix(1 0 -0.185 1 9.644 16.929)" fill="#ffffff"
d="M-1.508,-11.229 L0.746,-11.229 L-3.038,0.000 L-5.292,0.000 Z M5.292,0.000 L3.038,0.000 L-0.746,-11.229 L1.508,-11.229 Z M-2.739,-4.232 L2.739,-4.232 L3.285,-2.613 L-3.285,-2.613 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<!-- The B button of the menus' button keys (ui\shell\bitmaps\b_butn), redrawn: drawn
in the 32x32 bitmap's texels, where its art is (tools/button_assets.py). A tilted
red oval, dark at the top and bright at the bottom, with a pale rim, and its
letter in white, upright from its baseline and slanted. -->
<defs>
<!-- (along the oval's long axis, top to bottom) -->
<linearGradient id="body" gradientUnits="userSpaceOnUse" x1="0" y1="-13.053" x2="0" y2="13.053">
<stop offset="0" stop-color="#660f13"/>
<stop offset="0.5" stop-color="#b50000"/>
<stop offset="1" stop-color="#ff0200"/>
</linearGradient>
<linearGradient id="rim" gradientUnits="userSpaceOnUse" x1="0" y1="-13.053" x2="0" y2="13.053">
<stop offset="0" stop-color="#ffffff" stop-opacity="0.72"/>
<stop offset="1" stop-color="#ffffff" stop-opacity="0.79"/>
</linearGradient>
</defs>
<g transform="translate(10.193 12.921) rotate(-27.868)">
<ellipse id="oval" rx="9.041" ry="13.053" fill="url(#body)"/>
<ellipse rx="8.452" ry="12.464" fill="none" stroke="url(#rim)" stroke-width="1.178"/>
</g>
<path id="letter" transform="matrix(1 0 -0.185 1 3.807 16.894)" fill="#ffffff"
d="M0.000,-11.283 L5.649,-11.283 A3.265,3.265 0 0 1 8.914,-8.018 A3.265,3.265 0 0 1 5.649,-4.753 L0.000,-4.753 Z M0.000,-6.538 L6.095,-6.538 A3.238,3.238 0 0 1 9.333,-3.300 L9.333,-3.238 A3.238,3.238 0 0 1 6.095,0.000 L0.000,0.000 Z M2.188,-9.498 L2.188,-6.538 L5.246,-6.538 A1.480,1.480 0 0 0 6.726,-8.018 A1.480,1.480 0 0 0 5.246,-9.498 Z M2.188,-4.753 L2.188,-1.785 L6.095,-1.785 A1.050,1.050 0 0 0 7.145,-2.835 L7.145,-3.704 A1.050,1.050 0 0 0 6.095,-4.753 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<!-- The X button of the menus' button keys (ui\shell\bitmaps\x_butn), redrawn: drawn
in the 32x32 bitmap's texels, where its art is (tools/button_assets.py). A tilted
blue oval, dark at the top and bright at the bottom, with a pale rim, and its
letter in white, upright from its baseline and slanted. -->
<defs>
<!-- (along the oval's long axis, top to bottom) -->
<linearGradient id="body" gradientUnits="userSpaceOnUse" x1="0" y1="-12.932" x2="0" y2="12.932">
<stop offset="0" stop-color="#0d1c3d"/>
<stop offset="0.5" stop-color="#08266c"/>
<stop offset="1" stop-color="#05349d"/>
</linearGradient>
<linearGradient id="rim" gradientUnits="userSpaceOnUse" x1="0" y1="-12.932" x2="0" y2="12.932">
<stop offset="0" stop-color="#ffffff" stop-opacity="0.75"/>
<stop offset="1" stop-color="#ffffff" stop-opacity="0.74"/>
</linearGradient>
</defs>
<g transform="translate(10.341 12.879) rotate(-26.459)">
<ellipse id="oval" rx="8.659" ry="12.932" fill="url(#body)"/>
<ellipse rx="8.082" ry="12.355" fill="none" stroke="url(#rim)" stroke-width="1.154"/>
</g>
<path id="letter" transform="matrix(1 0 -0.185 1 8.621 16.043)" fill="#ffffff"
d="M-4.981,-10.970 L-2.302,-10.970 L5.237,0.000 L2.558,0.000 Z M2.302,-10.970 L4.981,-10.970 L-2.558,0.000 L-5.237,0.000 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+27
View File
@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<!-- The Y button of the menus' button keys (ui\shell\bitmaps\y_butn), redrawn: drawn
in the 32x32 bitmap's texels, where its art is (tools/button_assets.py). A tilted
yellow oval, dark at the top and bright at the bottom, with a pale rim, and its
letter in white, upright from its baseline and slanted,
casting a dark shadow down and to the right. -->
<defs>
<!-- (along the oval's long axis, top to bottom) -->
<linearGradient id="body" gradientUnits="userSpaceOnUse" x1="0" y1="-12.932" x2="0" y2="12.932">
<stop offset="0" stop-color="#504505"/>
<stop offset="0.5" stop-color="#bb7c03"/>
<stop offset="1" stop-color="#ffbc00"/>
</linearGradient>
<linearGradient id="rim" gradientUnits="userSpaceOnUse" x1="0" y1="-12.932" x2="0" y2="12.932">
<stop offset="0" stop-color="#ffffff" stop-opacity="0.71"/>
<stop offset="1" stop-color="#ffffff" stop-opacity="0.63"/>
</linearGradient>
</defs>
<g transform="translate(10.341 12.879) rotate(-26.459)">
<ellipse id="oval" rx="8.659" ry="12.932" fill="url(#body)"/>
<ellipse rx="8.082" ry="12.355" fill="none" stroke="url(#rim)" stroke-width="1.154"/>
</g>
<path transform="matrix(1 0 -0.185 1 10.387 16.978)" fill="#3c1e00" fill-opacity="0.60"
d="M-5.184,-11.167 L-2.927,-11.167 L1.047,-4.308 L-1.047,-4.308 Z M2.927,-11.167 L5.184,-11.167 L1.047,-4.308 L-1.047,-4.308 Z M-1.047,-4.908 L1.047,-4.908 L1.047,0.000 L-1.047,0.000 Z"/>
<path id="letter" transform="matrix(1 0 -0.185 1 8.887 15.778)" fill="#ffffff"
d="M-5.184,-11.167 L-2.927,-11.167 L1.047,-4.308 L-1.047,-4.308 Z M2.927,-11.167 L5.184,-11.167 L1.047,-4.308 L-1.047,-4.308 Z M-1.047,-4.908 L1.047,-4.908 L1.047,0.000 L-1.047,0.000 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+2 -2
View File
@@ -155,7 +155,7 @@ the setting for one start of the game. It has priority over the file.
| `display.interpolation` | `true` | `HALO_INTERPOLATION` | `true`: one frame for each refresh of the display. `false`: 30 frames each second, as on the Xbox. Refer to "Frame rate". |
| `display.direct_camera` | `true` | `HALO_DIRECT_CAMERA` | `true`: in first person, on foot, the view points where the player aims in each frame, not where the last tick left it. Refer to "Frame rate". |
| `display.high_res_hud` | `true` | `HALO_HIGH_RES_HUD` | `true`: the HUD (meters, counters, panels and their outlines, the motion sensor, reticles, waypoints, scopes) is drawn from the high-res assets in `port/assets/hud`, 8x the size of the maps' bitmaps. The bitmaps with English text keep the maps' own. `false`: the maps' own bitmaps. |
| `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the display's resolution, laid out as before, and the menus' titles are drawn from the high-res pictures in `port/assets/titles`. `false`: the maps' bitmap fonts and titles. |
| `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the display's resolution, laid out as before, the menus' titles are drawn from the high-res pictures in `port/assets/titles`, and the controller button icons from those in `port/assets/buttons`. `false`: the maps' bitmap fonts, titles and button icons. |
| `display.player_names` | `"all"` | `HALO_PLAYER_NAMES` | In multiplayer, whose names are drawn above their heads: `"all"`, `"allies"`, `"enemies"` or `"none"`. An ally's name is drawn above the triangle the game shows over teammates. An enemy's name shows only while the enemy is in sight and not camouflaged, so it never shows where an enemy hides, and only as far away as the weapon in hand turns its reticle red over an enemy (at least 20 world units, the motion sensor's reach, and at most 70). |
| `display.player_name_scale` | `1.0` | `HALO_PLAYER_NAME_SCALE` | How large the players' names are drawn: `1.0` is three quarters of the size of the HUD's text, from `0.25` to `4`. With high-res text, larger names are rasterized at their size, so they stay sharp. |
| `display.scoreboard_team_layout` | `"teams"` | `HALO_SCOREBOARD_TEAM_LAYOUT` | How the multiplayer scoreboard (hold BACK, or F1) lists a team game's players. `"teams"`: a column for each team, red on the left and blue on the right. `"score"`: all the players in order of score. With more players than fit, the mouse wheel and Page Up / Page Down scroll the scoreboard. |
@@ -478,7 +478,7 @@ The game sends the activity only to a Discord client of the same user.
| Game code | All 466 C files of the game. The changes are in "Game source changes". |
| Graphics | Direct3D 8 on OpenGL 4.5 core through SDL3 (`src/d3d8_gl.c`). The port translates the NV2A vertex shaders and register combiners to GLSL. It decodes all the Xbox texture formats. The vertex and index buffers come from a GL copy of the Xbox memory. |
| High-res HUD | The HUD is drawn from high-res assets: redraws at 8x the size of the maps' bitmaps (4x for the largest), in `port/assets/hud`. They cover the meters, counters, panels and their outlines, the motion sensor, reticles, waypoints and scopes, but no bitmap with English text. `tools/hud_assets.py` makes them from the SVG redraws, and the build embeds them in the executable. When the game uploads one of those bitmaps, `src/hud_hires.c` gives the high-res texture in its place, if the bitmap's pixels are those of the English maps: another language's maps keep their own. The game sizes and places the HUD from its tags as before. `display.high_res_hud = false` turns this off. |
| High-res text | The menus' and HUD's text is drawn with Overpass (`port/assets/fonts`, SIL Open Font License) in place of the maps' bitmap fonts, which are Interstate. `src/text_hires.c` rasterizes each glyph with stb_truetype (`port/third_party/stb`) at the display's resolution, into an atlas that a placeholder bitmap of the game stands for. The game lays the text out from its font tags as before. The menus' titles (the screens' headers and the main menu's items) are pictures of text in the maps, so they are drawn as the high-res HUD is: `tools/title_assets.py` sets each one again in OpenCE, Roger White's public-domain Newtown respaced to match the maps' commercial title typeface (`tools/title_font.py`), at 4x the bitmap's size over its own plate or glow, each letter placed where the map's letter is, in `port/assets/titles`. The postgame carnage report's title is set over a hand-made SVG redraw of its panel (`port/assets/titles/svg`) instead. `display.high_res_text = false` turns it off. |
| High-res text | The menus' and HUD's text is drawn with Overpass (`port/assets/fonts`, SIL Open Font License) in place of the maps' bitmap fonts, which are Interstate. `src/text_hires.c` rasterizes each glyph with stb_truetype (`port/third_party/stb`) at the display's resolution, into an atlas that a placeholder bitmap of the game stands for. The game lays the text out from its font tags as before. The menus' titles (the screens' headers and the main menu's items) are pictures of text in the maps, so they are drawn as the high-res HUD is: `tools/title_assets.py` sets each one again in OpenCE, Roger White's public-domain Newtown respaced to match the maps' commercial title typeface (`tools/title_font.py`), at 4x the bitmap's size over its own plate or glow, each letter placed where the map's letter is, in `port/assets/titles`. The postgame carnage report's title is set over a hand-made SVG redraw of its panel (`port/assets/titles/svg`) instead. The A, B, X and Y button icons beside the menus' key labels and in their text ("Press A to Join") are drawn from hand-made SVG redraws at 8x the size of the maps' bitmaps (`port/assets/buttons`, made by `tools/button_assets.py`), in the same sizes and places. `display.high_res_text = false` turns it off. |
| Sound | Xbox DirectSound on SDL3 audio (`src/dsound_sdl.c`): PCM and Xbox ADPCM, mixed at 48 kHz, with volume, pitch, mix bins, distance, stereo pan, occlusion and obstruction. There is no Doppler effect, no cones and no reverb. |
| Input | XInput on SDL3 (`src/xinput_sdl.c`): keyboard, mouse, gamepads with rumble, and the debug keyboard for the console. |
| Files | The Win32 file functions and the MSVC file functions on POSIX, with the translation of Xbox paths. |
+127 -2
View File
@@ -11,11 +11,20 @@ reused (when cache_block_index and base_address are cleared). The texture
cache of the platform layer uploads the pixels at an address whenever they
are written there, and asks hud_hires_asset_at which bitmap they are: a block
reused for another bitmap, or a map unloaded, is a write, which asks again.
A texture drawn for only some of a bitmap's sprites (hud_msg_icons_sm's
buttons) does not stand for its bitmap: the game draws those sprites from a
placeholder (hud_hires_sprite_bitmap), a bitmap of the same size and format,
so the same coordinates, made when first needed and deleted with the map;
the texture cache draws the texture for it (hud_hires_placeholder_texture).
When the texture cannot be drawn, the sprites are drawn from their bitmap.
*/
#include "cseries.h"
#include "bitmaps/bitmap_group.h"
#include "bitmaps/bitmap_group_lookup.h"
#include "bitmaps/bitmaps.h"
#include "rasterizer/xbox/rasterizer_xbox_hardware_bitmaps.h"
#include "tag_files/tag_groups.h"
/* the platform layer's (port/linux/src) */
@@ -24,16 +33,21 @@ long hud_hires_asset_count(void);
char const *hud_hires_asset_tag(long asset);
long hud_hires_asset_bitmap(long asset);
long hud_hires_asset_fits(long asset, long width, long height);
unsigned long hud_hires_asset_sprites(long asset);
int hud_hires_sprites_drawable(long asset, unsigned long address, unsigned long level0_size);
void hud_hires_register_placeholder(long asset, unsigned long const *texture);
void hud_hires_tags_loaded(void);
void hud_hires_tags_unloaded(void);
long hud_hires_asset_at(unsigned long address, long width, long height);
struct bitmap_data const *hud_hires_sprite_bitmap(struct bitmap_data const *bitmap, short sequence_index);
/* ---------- constants */
enum
{
MAXIMUM_HIRES_BITMAPS = 128,
MAXIMUM_HIRES_SPRITE_BITMAPS = 8,
};
/* ---------- globals */
@@ -45,6 +59,40 @@ static struct
} hires_bitmaps[MAXIMUM_HIRES_BITMAPS];
static long hires_bitmap_count = 0;
/* the bitmaps some of whose sprites have a texture, with their placeholder,
and whether the texture can be drawn for the pixels at checked_address
(checked again when the texture cache loads them elsewhere) */
static struct
{
struct bitmap_data *bitmap;
long asset;
struct bitmap_data *placeholder;
boolean placeholder_failed;
void *checked_address;
boolean drawable;
} hires_sprite_bitmaps[MAXIMUM_HIRES_SPRITE_BITMAPS];
static long hires_sprite_bitmap_count = 0;
/* ---------- private code */
static void hud_hires_sprite_bitmaps_forget(
void)
{
long index;
for (index = 0; index < hires_sprite_bitmap_count; index++)
{
if (hires_sprite_bitmaps[index].placeholder)
{
hud_hires_register_placeholder(hires_sprite_bitmaps[index].asset, NULL);
bitmap_delete(hires_sprite_bitmaps[index].placeholder);
}
}
hires_sprite_bitmap_count = 0;
return;
}
/* ---------- public code */
void hud_hires_tags_loaded(
@@ -55,6 +103,7 @@ void hud_hires_tags_loaded(
long missing = 0;
hires_bitmap_count = 0;
hud_hires_sprite_bitmaps_forget();
for (asset = 0; asset < asset_count && hires_bitmap_count < MAXIMUM_HIRES_BITMAPS; asset++)
{
long group_index = tag_loaded(BITMAP_GROUP_TAG, hud_hires_asset_tag(asset));
@@ -71,6 +120,16 @@ void hud_hires_tags_loaded(
platform_log("high-res hud: %s bitmap %ld is %dx%d here, which its texture does not fit",
hud_hires_asset_tag(asset), hud_hires_asset_bitmap(asset), bitmap->width, bitmap->height);
}
else if (hud_hires_asset_sprites(asset))
{
if (hires_sprite_bitmap_count < MAXIMUM_HIRES_SPRITE_BITMAPS)
{
csmemset(&hires_sprite_bitmaps[hires_sprite_bitmap_count], 0, sizeof(hires_sprite_bitmaps[0]));
hires_sprite_bitmaps[hires_sprite_bitmap_count].bitmap = bitmap;
hires_sprite_bitmaps[hires_sprite_bitmap_count].asset = asset;
hires_sprite_bitmap_count++;
}
}
else
{
hires_bitmaps[hires_bitmap_count].bitmap = bitmap;
@@ -78,8 +137,8 @@ void hud_hires_tags_loaded(
hires_bitmap_count++;
}
}
platform_log("high-res hud: %ld of %ld bitmaps in this map (%ld not in it)",
hires_bitmap_count, asset_count, missing);
platform_log("high-res hud: %ld of %ld bitmaps in this map, %ld for some of their sprites (%ld not in it)",
hires_bitmap_count + hires_sprite_bitmap_count, asset_count, hires_sprite_bitmap_count, missing);
return;
}
@@ -88,10 +147,76 @@ void hud_hires_tags_unloaded(
void)
{
hires_bitmap_count = 0;
hud_hires_sprite_bitmaps_forget();
return;
}
struct bitmap_data const *hud_hires_sprite_bitmap(
struct bitmap_data const *bitmap,
short sequence_index)
{
long index;
for (index = 0; index < hires_sprite_bitmap_count; index++)
{
struct bitmap_data *sprite_bitmap = hires_sprite_bitmaps[index].bitmap;
if (sprite_bitmap != bitmap ||
sequence_index < 0 ||
sequence_index >= 32 ||
!(hud_hires_asset_sprites(hires_sprite_bitmaps[index].asset) & (1UL << sequence_index)))
{
continue;
}
/* (checked on its pixels in the texture cache, once they are loaded) */
if (sprite_bitmap->cache_block_index == NONE || !sprite_bitmap->base_address)
break;
if (hires_sprite_bitmaps[index].checked_address != sprite_bitmap->base_address)
{
hires_sprite_bitmaps[index].checked_address = sprite_bitmap->base_address;
hires_sprite_bitmaps[index].drawable = hud_hires_sprites_drawable(
hires_sprite_bitmaps[index].asset,
(unsigned long)sprite_bitmap->base_address,
(unsigned long)bitmap_mipmap_get_pixel_data_size(sprite_bitmap, 0)) != 0;
}
if (hires_sprite_bitmaps[index].drawable &&
!hires_sprite_bitmaps[index].placeholder &&
!hires_sprite_bitmaps[index].placeholder_failed)
{
struct bitmap_data *placeholder = bitmap_2d_new(
sprite_bitmap->width,
sprite_bitmap->height,
0,
sprite_bitmap->format);
if (placeholder && !rasterizer_bitmap_new(placeholder))
{
bitmap_delete(placeholder);
placeholder = NULL;
}
if (placeholder)
{
hud_hires_register_placeholder(
hires_sprite_bitmaps[index].asset,
(unsigned long const *)placeholder->hardware_format);
}
else
{
platform_log("high-res hud: no placeholder for %s's sprites: drawn as they are",
hud_hires_asset_tag(hires_sprite_bitmaps[index].asset));
hires_sprite_bitmaps[index].placeholder_failed = TRUE;
}
hires_sprite_bitmaps[index].placeholder = placeholder;
}
if (hires_sprite_bitmaps[index].drawable && hires_sprite_bitmaps[index].placeholder)
return hires_sprite_bitmaps[index].placeholder;
break;
}
return bitmap;
}
long hud_hires_asset_at(
unsigned long address,
long width,
+86 -4
View File
@@ -9,13 +9,14 @@ port/linux/game/hud_hires_tags.c). Each texture is decoded from its PNG when
first drawn and kept: up to 69 of the HUD's, about 225 MB with their mip
levels, though a game draws only some (the scopes' only when zoomed), and
the titles of the menus shown, about 3 MB each (11 MB for the carnage
report's, a whole panel).
report's, a whole panel), and their button icons, about 0.3 MiB each (5.3
MiB for the message icons' sheet).
They are drawn with linear filtering and their mip levels (d3d8_gl.c,
configure_sampler), as they are larger than they appear.
The PNGs are the ones tools/hud_assets.py and title_assets.py write, so only
what they write is read: 8-bit RGBA, not interlaced, its data inflated with
the game's zlib.
The PNGs are the ones tools/hud_assets.py, title_assets.py and
button_assets.py write, so only what they write is read: 8-bit RGBA, not
interlaced, its data inflated with the game's zlib.
*/
#include "hud_hires.h"
@@ -103,6 +104,87 @@ int hud_hires_override_coverage(long asset)
return asset >= 0 && asset < hud_hires_asset_count() && hud_hires_embedded[asset].coverage;
}
/* ---------- sprites */
/* the placeholders of the textures drawn for some of a bitmap's sprites
(port/linux/game/hud_hires_tags.c): their D3D textures' Data */
#define MAXIMUM_PLACEHOLDERS 8
static struct
{
unsigned long data;
long asset;
} placeholders[MAXIMUM_PLACEHOLDERS];
static long placeholder_count = 0;
/* the sequences whose sprites the texture is drawn for (hud_hires.h), or 0 */
unsigned long hud_hires_asset_sprites(long asset)
{
return hud_hires_embedded[asset].sprites;
}
/* whether the texture drawn for some of a bitmap's sprites can be drawn for
the bitmap whose pixels are at address (guest virtual; its first mip level
level0_size bytes): its setting on, the pixels those it was drawn for, and
the texture decoded */
int hud_hires_sprites_drawable(long asset, unsigned long address, unsigned long level0_size)
{
static int hud_enabled = -1, titles_enabled = -1;
unsigned long levels;
if (hud_enabled < 0)
{
hud_enabled = config_boolean("display.high_res_hud");
titles_enabled = config_boolean("display.high_res_text");
}
if (asset < 0 || asset >= hud_hires_asset_count() || !hud_hires_embedded[asset].sprites ||
!(hud_hires_embedded[asset].title ? titles_enabled : hud_enabled))
{
return 0;
}
if (crc32(0L, (const Bytef *)address, (uInt)level0_size) != hud_hires_embedded[asset].crc)
{
if (!textures[asset].other_pixels_logged)
{
platform_log("high-res hud: %s bitmap %d is not the one its sprites' texture was drawn for "
"here (another language's or a modified map): drawn as it is",
hud_hires_embedded[asset].tag, hud_hires_embedded[asset].bitmap);
textures[asset].other_pixels_logged = 1;
}
return 0;
}
return hud_hires_override_texture(asset, &levels) != 0;
}
/* the placeholder the game draws the texture's sprites from: its D3D
texture (NULL forgets it) */
void hud_hires_register_placeholder(long asset, const unsigned long *texture)
{
long index;
for (index = 0; index < placeholder_count && placeholders[index].asset != asset; index++)
;
if (texture && index == placeholder_count && placeholder_count < MAXIMUM_PLACEHOLDERS)
placeholder_count++;
if (index < placeholder_count)
{
placeholders[index].data = texture ? texture[1] : 0;
placeholders[index].asset = asset;
}
}
unsigned int hud_hires_placeholder_texture(unsigned long data, unsigned long *levels)
{
long index;
for (index = 0; data && index < placeholder_count; index++)
{
if (placeholders[index].data == data)
return hud_hires_override_texture(placeholders[index].asset, levels);
}
return 0;
}
/* ---------- decoding */
static unsigned long big_endian_long(const unsigned char *bytes)
+20 -5
View File
@@ -5,11 +5,18 @@ The high-res HUD: textures drawn from the hand-made SVG redraws of the Halo PC
HUD sheets (port/assets/hud, made by tools/hud_assets.py), each 8x the size
of the bitmap of the Xbox maps it stands for (4x for the largest) and in that
bitmap's layout. The menus' titles, pictures of text in the maps, are drawn
the same way (port/assets/titles, made by tools/title_assets.py).
the same way (port/assets/titles, made by tools/title_assets.py), and so are
their controller button icons (port/assets/buttons, made by
tools/button_assets.py).
tools/embed_assets.py makes them C data (hud_hires_embedded); hud_hires.c
decodes them, and the texture cache (xbox_textures.c) draws one in place of
its bitmap whenever that bitmap's pixels are uploaded: the game still sizes
and places the bitmap by its tag, so nothing else changes.
A texture may instead stand for only some of a bitmap's sprites (the
buttons among the message icons the menus set into their text): the game
draws those sprites from a placeholder bitmap of the same size
(port/linux/game/hud_hires_tags.c), which the texture cache draws the
texture for, and the bitmap's other sprites from the bitmap as before.
*/
#ifndef HUD_HIRES_H
@@ -19,9 +26,12 @@ and places the bitmap by its tag, so nothing else changes.
bitmap group tag's name, its index there, and the CRC-32 of its first mip
level's pixels as the English maps have them); coverage: a meter's, whose
green is how much of each texel its shapes cover (the meter shader reads
only its blue and alpha); title: a menu's title (port/assets/titles, made by
tools/title_assets.py), drawn with display.high_res_text rather than
display.high_res_hud */
only its blue and alpha); title: the menus', a title (port/assets/titles,
made by tools/title_assets.py) or a button icon (port/assets/buttons, made
by tools/button_assets.py), drawn with display.high_res_text rather than
display.high_res_hud; sprites: the sequences of its bitmap's group (a bit
each) whose sprites it is drawn for, through a placeholder, or 0: the whole
bitmap, in its place */
struct hud_hires_embedded
{
const char *tag;
@@ -30,6 +40,7 @@ struct hud_hires_embedded
unsigned int crc;
int coverage;
int title;
unsigned int sprites;
const unsigned int *png;
unsigned int png_size;
};
@@ -40,7 +51,7 @@ extern const unsigned int hud_hires_embedded_count;
/* the texture standing for the bitmap whose pixels are uploaded from address
(guest virtual) with this size, its first mip level being level0_size bytes,
or -1: none, its setting (display.high_res_hud, or display.high_res_text for
a title) off, or pixels other than those the
the menus') off, or pixels other than those the
texture was drawn for (another language's maps, which have their own text,
or modified ones) */
long hud_hires_override_find(unsigned long address, unsigned long width, unsigned long height,
@@ -50,5 +61,9 @@ could not be), and the number of its mip levels */
unsigned int hud_hires_override_texture(long asset, unsigned long *levels);
/* whether its green is its coverage (d3d8_gl.c, nv2a_psh.c: coverage_alpha) */
int hud_hires_override_coverage(long asset);
/* the GL texture of the texture drawn for some of a bitmap's sprites whose
placeholder's D3D texture has this Data, and the number of its mip levels;
0 if data is no placeholder's */
unsigned int hud_hires_placeholder_texture(unsigned long data, unsigned long *levels);
#endif
+11
View File
@@ -714,6 +714,17 @@ static GLuint texture_entry_result(struct texture_entry *entry, GLenum *target,
return atlas;
}
}
/* (a high-res texture drawn for some sprites, for the placeholder the
game draws them from: hud_hires.h) */
{
GLuint texture = hud_hires_placeholder_texture(entry->data, &description->levels);
if (texture)
{
description->hires = TRUE;
return texture;
}
}
if (entry->override >= 0)
{
GLuint texture = hud_hires_override_texture(entry->override, &description->levels);
+4
View File
@@ -3827,6 +3827,10 @@ static void render_state_bitmap(
if (bitmap && _texture_cache_bitmap_get_hardware_format(
(struct bitmap_data *)bitmap, FALSE, TRUE))
{
/* port: the sprite from a high-res texture, if it has one (port/linux/game/hud_hires_tags.c) */
extern struct bitmap_data const *hud_hires_sprite_bitmap(struct bitmap_data const *sheet, short sequence_index);
bitmap = hud_hires_sprite_bitmap(bitmap, icon->sequence_index);
scale = hud_globals_get_scale(local_player_count() > 1);
point.x = (short)(icon->offset.x * scale + cursor_bounds->x0 + 1.0f);
point.y = (short)(cursor_bounds->y1 - icon->offset.y * scale - 2.0f);
+303
View File
@@ -0,0 +1,303 @@
#!/usr/bin/env python3
"""Makes high-res textures of the menus' controller button icons
(port/assets/buttons/*.png) from hand-drawn SVG redraws of them
(port/assets/buttons/svg):
python tools/button_assets.py layout --map assets/maps/ui.map
python tools/button_assets.py build
python tools/button_assets.py check --map assets/maps/ui.map --out /tmp/button_check
The icons beside the menus' key labels (A SELECT, B BACK, X DELETE, Y CREATE
NEW) are 32x32 bitmaps of ui.map with their art in the top left corner,
which the widgets' backgrounds draw at one texel to the pixel
(draw_bitmap_in_rect crops a bitmap larger than its widget, in coordinates
of the bitmap's size); the dialogs, the pause menu and the help screens draw
smaller A and B icons, bitmaps of their own in every map. So a texture of 8x
a bitmap's size, in its layout, draws in its place unchanged, as the
high-res HUD's do (port/linux/src/hud_hires.c), and with the high-res text
(display.high_res_text), as the menus' titles do.
Each redraw is drawn in the texels (viewBox 0 0 32 32) of the large icon it
is named after, over the map's art: a tilted oval with a pale rim, dark at
the top and bright at the bottom, and its letter in white (Y's casting a
dark shadow). A small icon is the same art smaller.
The menus' text draws the same buttons as message icons ("Press %a-button
to Join": draw_string_and_hack_in_icons, ui_widget.c), sprites of a sheet
of every map, hud_msg_icons_sm, that the game tints: stencils, each oval in
white with its letter cut out. They are drawn from the redraws' oval and
letter (their ids "oval" and "letter"), into a texture of the sheet's
layout that has only those sprites: the sheet's other icons (triggers and
sticks, with English words) keep the map's, so the texture stands for the
sprites alone, and the game draws them from it through a placeholder
(port/linux/game/hud_hires_tags.c).
layout: reads each icon's bitmap from the map (its size, format and the CRC
of its pixels, which the game checks before drawing the texture in its
place) and places its redraw on it: a small icon's scaled and moved as
the map's small art is from its large art (so that their shapes' area
and centre are the same), and a message icon's stencil where it covers
the sprite's best. Writes port/assets/buttons/buttons.json.
build: renders the redraws as buttons.json places them into
port/assets/buttons/*.png (committed; the builds embed them:
tools/embed_assets.py).
check: compares each PNG, reduced to its bitmap's size, with the map's
bitmap and writes side-by-side images into --out.
Needs rsvg-convert, Pillow, NumPy and SciPy.
"""
import argparse
import json
import re
import sys
import tempfile
import xml.etree.ElementTree as ElementTree
import zlib
from pathlib import Path
import numpy as np
from PIL import Image
sys.path.insert(0, str(Path(__file__).resolve().parent))
from hud_assets import (FORMATS, XboxMap, bleed, decode_bitmap, level0_size, overlap, # noqa: E402
pixel_rectangle, render_svg)
ROOT = Path(__file__).resolve().parent.parent
ASSETS = ROOT / "port/assets/buttons"
LIST = ASSETS / "buttons.json"
SCALE = 8
BITMAPS = "ui\\shell\\bitmaps\\"
# each icon's bitmap and its redraw (drawn over the bitmap it is named
# after): the button keys' (ui.map), and the smaller ones of the dialogs,
# the pause menu and the help screens (every map)
BUTTONS = {
BITMAPS + "a_butn": "a_butn.svg",
BITMAPS + "b_butn": "b_butn.svg",
BITMAPS + "x_butn": "x_butn.svg",
BITMAPS + "y_butn": "y_butn.svg",
BITMAPS + "a_butn_sm": "a_butn.svg",
BITMAPS + "b_butn_sm": "b_butn.svg",
}
# the message icons' sheet, and the redraw of each of its buttons, by the
# sequence of its sprite (on the sheet's first bitmap)
MESSAGE_ICONS = "ui\\hud\\bitmaps\\hud_msg_icons_sm"
MESSAGE_BUTTONS = {0: "a_butn.svg", 1: "b_butn.svg", 2: "x_butn.svg", 3: "y_butn.svg"}
def document(width: int, height: int, body: str) -> str:
return (f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" '
f'viewBox="0 0 {width} {height}">\n{body}\n</svg>\n')
def placed(svg: Path, width: int, height: int, place: list) -> str:
"""The redraw as a picture of its bitmap's size, scaled by place[0] and
moved by place[1:] (texels)."""
body = re.search(r"<svg\b[^>]*>(.*)</svg>", svg.read_text(), re.S).group(1)
scale, x, y = place
return document(width, height, f'<g transform="translate({x} {y}) scale({scale})">{body}</g>')
def stencil(svg: str, cell: list, place: list, index: int) -> str:
"""A redraw as the message icons draw its button, in a sprite's cell
(texels of its sheet): its oval in white with its letter cut out,
scaled by place[0] and moved by place[1:] from the cell's corner, and
kept to the cell; index names its clip and mask."""
root = ElementTree.parse(ASSETS / "svg" / svg).getroot()
parents = {child: parent for parent in root.iter() for child in parent}
oval, letter = root.find(".//*[@id='oval']"), root.find(".//*[@id='letter']")
left, top, right, bottom = cell
scale, x, y = place
moved = f"translate({left + x} {top + y}) scale({scale})"
area = f'x="{left}" y="{top}" width="{right - left}" height="{bottom - top}"'
return (f'<defs><clipPath id="cell{index}"><rect {area}/></clipPath>\n'
f'<mask id="letter{index}" maskUnits="userSpaceOnUse" {area}><rect {area} fill="#ffffff"/>\n'
f'<path transform="{moved} {letter.get("transform")}" fill="#000000" d="{letter.get("d")}"/></mask></defs>\n'
f'<g clip-path="url(#cell{index})" mask="url(#letter{index})">'
f'<ellipse transform="{moved} {parents[oval].get("transform")}" rx="{oval.get("rx")}" '
f'ry="{oval.get("ry")}" fill="#ffffff"/></g>')
def render(text: str, scale: int) -> np.ndarray:
"""An SVG document at scale times its size."""
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "placed.svg"
path.write_text(text)
return render_svg(path, scale)
def icon(xbox_map: XboxMap, tag: str) -> dict:
"""An icon's bitmap (its group's only one)."""
if ("bitm", tag) not in xbox_map.tags:
sys.exit(f"{tag}: not in this map")
bitmaps = xbox_map.bitmap_group(tag)["bitmaps"]
if len(bitmaps) != 1:
sys.exit(f"{tag}: {len(bitmaps)} bitmaps, not the icon's one")
return bitmaps[0]
def shape(alpha: np.ndarray, scale: int = 1) -> tuple:
"""The area and centre of a shape, in texels, from its alpha drawn at
scale times the texels."""
alpha = alpha.astype(float) / 255
ys, xs = np.mgrid[0:alpha.shape[0], 0:alpha.shape[1]]
area = alpha.sum()
return (area / scale ** 2, ((xs + 0.5) * alpha).sum() / area / scale,
((ys + 0.5) * alpha).sum() / area / scale)
def stencil_place(svg: str, alpha: np.ndarray) -> list:
"""Where a redraw's stencil goes in a sprite's cell, whose alpha it is:
where it covers it best, from where it has its area and centre."""
from scipy import optimize
height, width = alpha.shape
root = ElementTree.parse(ASSETS / "svg" / svg).getroot()
size = int(root.get("width")), int(root.get("height"))
area, x, y = shape(render(document(*size, stencil(svg, [0, 0, *size], [1, 0, 0], 0)), SCALE)[..., 3], SCALE)
sprite_area, sprite_x, sprite_y = shape(alpha)
scale = (sprite_area / area) ** 0.5
target = alpha.astype(float) / 255
def error(place: np.ndarray) -> float:
drawn = render(document(width, height, stencil(svg, [0, 0, width, height], list(place), 0)), SCALE)
reduced = drawn[..., 3].astype(float).reshape(height, SCALE, width, SCALE).mean(axis=(1, 3)) / 255
return float(((reduced - target) ** 2).sum())
best = optimize.minimize(error, [scale, sprite_x - scale * x, sprite_y - scale * y], method="Powell",
options={"xtol": 1e-3, "ftol": 1e-7}).x
return [round(float(best[0]), 4), round(float(best[1]), 3), round(float(best[2]), 3)]
def layout(arguments) -> None:
xbox_map = XboxMap(Path(arguments.map))
entries = []
for tag, svg in BUTTONS.items():
bitmap = icon(xbox_map, tag)
width, height = bitmap["width"], bitmap["height"]
drawn_over = BITMAPS + Path(svg).stem
place = [1, 0, 0]
if drawn_over != tag:
area, x, y = shape(decode_bitmap(bitmap)[..., 3])
drawn_area, drawn_x, drawn_y = shape(decode_bitmap(icon(xbox_map, drawn_over))[..., 3])
scale = (area / drawn_area) ** 0.5
place = [round(scale, 4), round(x - scale * drawn_x, 3), round(y - scale * drawn_y, 3)]
entries.append({
"name": tag.split("\\")[-1] + "__0",
"tag": tag,
"bitmap": 0,
"width": width,
"height": height,
"format": FORMATS[bitmap["format"]],
"scale": SCALE,
# (the map's, which the game checks before drawing this in its
# place: other languages' maps and modified ones may differ)
"crc": zlib.crc32(bitmap["pixels"][:level0_size(bitmap)]),
"svg": svg,
"place": place,
})
print(f"{tag}: {width}x{height} {FORMATS[bitmap['format']]}, {svg} at {place[0]}x, moved {place[1]}, {place[2]}")
# the message icons' buttons
if ("bitm", MESSAGE_ICONS) not in xbox_map.tags:
sys.exit(f"{MESSAGE_ICONS}: not in this map")
group = xbox_map.bitmap_group(MESSAGE_ICONS)
bitmap = group["bitmaps"][0]
width, height = bitmap["width"], bitmap["height"]
alpha = decode_bitmap(bitmap)[..., 3]
sprites = []
for sequence, svg in MESSAGE_BUTTONS.items():
cells = group["sequences"][sequence]
if len(cells) != 1 or cells[0][0] != 0:
sys.exit(f"{MESSAGE_ICONS}: sequence {sequence} is not one sprite of its first bitmap")
cell = pixel_rectangle(cells[0], width, height)
place = stencil_place(svg, alpha[cell[1]:cell[3], cell[0]:cell[2]])
sprites.append({"sequence": sequence, "cell": cell, "svg": svg, "place": place})
print(f"{MESSAGE_ICONS} sequence {sequence}: cell {cell}, {svg} at {place[0]}x, moved {place[1]}, {place[2]}")
entries.append({
"name": MESSAGE_ICONS.split("\\")[-1] + "__0",
"tag": MESSAGE_ICONS,
"bitmap": 0,
"width": width,
"height": height,
"format": FORMATS[bitmap["format"]],
"scale": SCALE,
"crc": zlib.crc32(bitmap["pixels"][:level0_size(bitmap)]),
# (the sprites it stands for, not the whole bitmap)
"sprites": sprites,
})
LIST.write_text(json.dumps({"assets": entries}, indent=1) + "\n")
def build(arguments) -> None:
description = json.loads(LIST.read_text())
names = {f"{entry['name']}.png" for entry in description["assets"]}
for stale in ASSETS.glob("*.png"):
if stale.name not in names:
stale.unlink()
for entry in description["assets"]:
if "sprites" in entry:
body = "\n".join(stencil(sprite["svg"], sprite["cell"], sprite["place"], index)
for index, sprite in enumerate(entry["sprites"]))
image = render(document(entry["width"], entry["height"], body), entry["scale"])
# (grey the same as alpha, as the sheet's)
image[..., :3] = image[..., 3:4]
else:
image = render(placed(ASSETS / "svg" / entry["svg"], entry["width"], entry["height"], entry["place"]),
entry["scale"])
# (the colour carried into the transparent texels, so that
# filtering and the mip levels keep it at the oval's edge)
image = bleed(image)
Image.fromarray(image, "RGBA").save(ASSETS / f"{entry['name']}.png", optimize=True)
print(f"{entry['name']}.png: {image.shape[1]}x{image.shape[0]}")
def check(arguments) -> None:
xbox_map = XboxMap(Path(arguments.map))
output = Path(arguments.out)
output.mkdir(parents=True, exist_ok=True)
for entry in json.loads(LIST.read_text())["assets"]:
if ("bitm", entry["tag"]) not in xbox_map.tags:
print(f"{entry['name']}: not in this map")
continue
bitmap = xbox_map.bitmap_group(entry["tag"])["bitmaps"][entry["bitmap"]]
if zlib.crc32(bitmap["pixels"][:level0_size(bitmap)]) != entry["crc"]:
print(f"{entry['name']}: this map's bitmap is not the one laid out")
xbox = decode_bitmap(bitmap)
image = Image.open(ASSETS / f"{entry['name']}.png").convert("RGBA")
reduced = np.asarray(image.resize((entry["width"], entry["height"]), Image.BOX))
scale = entry["scale"]
# (each sprite's cell, or the whole bitmap)
cells = [sprite["cell"] for sprite in entry.get("sprites", [])] or [[0, 0, entry["width"], entry["height"]]]
for index, (left, top, right, bottom) in enumerate(cells):
score = overlap(reduced[top:bottom, left:right, 3].astype(float), xbox[top:bottom, left:right, 3].astype(float))
name = entry["name"] + (f"__sprite{index}" if "sprites" in entry else "")
print(f"{name}: alpha overlap {score:.2f}")
# the map's | ours reduced | ours, over the menus' dark blue
ours = image.crop((left * scale, top * scale, right * scale, bottom * scale))
panels = [Image.fromarray(np.ascontiguousarray(picture[top:bottom, left:right]), "RGBA").resize(
ours.size, Image.NEAREST) for picture in (xbox, reduced)]
panels.append(ours)
width, height = ours.size
sheet = Image.new("RGB", (width * 3 + 16, height), (255, 0, 0))
for place, picture in enumerate(panels):
background = Image.new("RGBA", picture.size, (16, 26, 60, 255))
sheet.paste(Image.alpha_composite(background, picture).convert("RGB"), (place * (width + 8), 0))
sheet.save(output / f"{name}.png")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
commands = parser.add_subparsers(dest="command", required=True)
command = commands.add_parser("layout")
command.add_argument("--map", required=True)
commands.add_parser("build")
command = commands.add_parser("check")
command.add_argument("--map", required=True)
command.add_argument("--out", required=True)
arguments = parser.parse_args()
{"layout": layout, "build": build, "check": check}[arguments.command](arguments)
if __name__ == "__main__":
sys.exit(main())
+19 -8
View File
@@ -1,14 +1,15 @@
#!/usr/bin/env python3
"""Embeds the high-res HUD textures (port/assets/hud, made by
tools/hud_assets.py), the menus' titles (port/assets/titles, made by
tools/title_assets.py) and the fonts the text is drawn with
tools/title_assets.py), their controller button icons (port/assets/buttons,
made by tools/button_assets.py) and the fonts the text is drawn with
(port/assets/fonts) in the game as C data:
python tools/embed_assets.py OUTPUT.c
writes OUTPUT.c with each PNG and the bitmap it stands for (its tag, index
and the checksum of its pixels, from port/assets/hud/layout.json and
port/assets/titles/titles.json), as
and the checksum of its pixels, from port/assets/hud/layout.json,
port/assets/titles/titles.json and port/assets/buttons/buttons.json), as
port/linux/src/hud_hires.h declares them. The builds generate it
(hud_assets_build, called by tools/linux_build.py, windows_build.py and
android_build.py), so the PNGs are the committed source and Android needs
@@ -30,6 +31,8 @@ HUD_ASSETS = Path("port/assets/hud")
LAYOUT = HUD_ASSETS / "layout.json"
TITLE_ASSETS = Path("port/assets/titles")
TITLE_LIST = TITLE_ASSETS / "titles.json"
BUTTON_ASSETS = Path("port/assets/buttons")
BUTTON_LIST = BUTTON_ASSETS / "buttons.json"
FONT_ASSETS = Path("port/assets/fonts")
FONT_LIST = FONT_ASSETS / "fonts.json"
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
@@ -45,9 +48,11 @@ def font_files() -> List[str]:
def textures() -> List[tuple]:
"""The textures: each one's folder, its entry in its list, and whether
it is a title."""
it is the menus' (a title or a button icon, drawn with the high-res
text)."""
result = []
for folder, listing, title in ((HUD_ASSETS, LAYOUT, False), (TITLE_ASSETS, TITLE_LIST, True)):
for folder, listing, title in ((HUD_ASSETS, LAYOUT, False), (TITLE_ASSETS, TITLE_LIST, True),
(BUTTON_ASSETS, BUTTON_LIST, True)):
if (ROOT / listing).is_file():
result += [(folder, asset, title) for asset in json.loads((ROOT / listing).read_text())["assets"]]
return result
@@ -55,7 +60,7 @@ def textures() -> List[tuple]:
def hud_asset_inputs() -> List[Path]:
"""The files the generated source is made from."""
inputs = [listing for listing in (LAYOUT, TITLE_LIST, FONT_LIST) if (ROOT / listing).is_file()]
inputs = [listing for listing in (LAYOUT, TITLE_LIST, BUTTON_LIST, FONT_LIST) if (ROOT / listing).is_file()]
if not inputs:
return []
return [*inputs, *(folder / f"{asset['name']}.png" for folder, asset, _ in textures()),
@@ -67,7 +72,8 @@ def hud_configure_inputs() -> List[Path]:
folders, for files added or removed), not each file, which a change of a
list may rename or remove."""
inputs = []
for folder, listing in ((HUD_ASSETS, LAYOUT), (TITLE_ASSETS, TITLE_LIST), (FONT_ASSETS, FONT_LIST)):
for folder, listing in ((HUD_ASSETS, LAYOUT), (TITLE_ASSETS, TITLE_LIST), (BUTTON_ASSETS, BUTTON_LIST),
(FONT_ASSETS, FONT_LIST)):
if (ROOT / listing).is_file():
inputs += [folder, listing]
return inputs
@@ -130,8 +136,13 @@ def main() -> None:
lines.append("")
tag = asset["tag"].replace("\\", "\\\\")
coverage = int(any(cell["kind"] == "meter" for cell in asset.get("cells", [])))
# (the sequences whose sprites it stands for, a bit each of 32; 0: the whole bitmap)
sequences = [sprite["sequence"] for sprite in asset.get("sprites", [])]
if any(not 0 <= sequence < 32 for sequence in sequences):
sys.exit(f"{name}: sprite sequences must be 0 to 31 (hud_hires.h: sprites)")
sprites = sum(1 << sequence for sequence in sequences)
table.append(f'\t{{ "{tag}", {asset["bitmap"]}, {width}, {height}, 0x{asset["crc"]:08x}u, {coverage}, '
f'{int(title)}, asset{index}, {len(data)} }},')
f'{int(title)}, 0x{sprites:x}u, asset{index}, {len(data)} }},')
lines.append("const struct hud_hires_embedded hud_hires_embedded[] =")
lines.append("{")
lines.extend(table)