Add high-resolution UI controller buttons
No files matched your search
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -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
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -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. |
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
|
||||