diff --git a/AUTHORS.md b/AUTHORS.md
index 870122ff..59e4f483 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -6,6 +6,10 @@
and the desktop windows' icon): by ORION (`port/assets/icon`).
- **The controller's A, B, X and Y buttons** (the menus' button icons and
the HUD messages'): redraws by saulob (`port/assets/buttons`).
+- **The high-res HUD** (its meters, icons, scopes and messages, and the
+ weapons' reticles): redraws by MrBruh (`port/assets/hud`), based on
+ reference photos by Aerocatia. The sniper rifle's zoom labels (2x, 10x)
+ are set in Overpass (`port/assets/fonts`).
## Third-party art
diff --git a/port/assets/hud/hud_reticles__0.png b/port/assets/hud/hud_reticles__0.png
index 2bfd275e..46e88928 100644
Binary files a/port/assets/hud/hud_reticles__0.png and b/port/assets/hud/hud_reticles__0.png differ
diff --git a/port/assets/hud/hud_reticles__1.png b/port/assets/hud/hud_reticles__1.png
index 4a835216..fc3fef34 100644
Binary files a/port/assets/hud/hud_reticles__1.png and b/port/assets/hud/hud_reticles__1.png differ
diff --git a/port/assets/hud/hud_reticles__10.png b/port/assets/hud/hud_reticles__10.png
index ed65dab6..44938edd 100644
Binary files a/port/assets/hud/hud_reticles__10.png and b/port/assets/hud/hud_reticles__10.png differ
diff --git a/port/assets/hud/hud_reticles__11.png b/port/assets/hud/hud_reticles__11.png
index 8850f4e0..0dd2c291 100644
Binary files a/port/assets/hud/hud_reticles__11.png and b/port/assets/hud/hud_reticles__11.png differ
diff --git a/port/assets/hud/hud_reticles__3.png b/port/assets/hud/hud_reticles__3.png
index 1492dd38..7c8eafbc 100644
Binary files a/port/assets/hud/hud_reticles__3.png and b/port/assets/hud/hud_reticles__3.png differ
diff --git a/port/assets/hud/hud_reticles__4.png b/port/assets/hud/hud_reticles__4.png
index 86586103..7c70f179 100644
Binary files a/port/assets/hud/hud_reticles__4.png and b/port/assets/hud/hud_reticles__4.png differ
diff --git a/port/assets/hud/hud_reticles__5.png b/port/assets/hud/hud_reticles__5.png
index c7758178..5967622f 100644
Binary files a/port/assets/hud/hud_reticles__5.png and b/port/assets/hud/hud_reticles__5.png differ
diff --git a/port/assets/hud/hud_reticles__6.png b/port/assets/hud/hud_reticles__6.png
index eac87c80..720025f1 100644
Binary files a/port/assets/hud/hud_reticles__6.png and b/port/assets/hud/hud_reticles__6.png differ
diff --git a/port/assets/hud/hud_reticles__7.png b/port/assets/hud/hud_reticles__7.png
new file mode 100644
index 00000000..914a031e
Binary files /dev/null and b/port/assets/hud/hud_reticles__7.png differ
diff --git a/port/assets/hud/hud_reticles__8.png b/port/assets/hud/hud_reticles__8.png
index 17c9f787..e07634ed 100644
Binary files a/port/assets/hud/hud_reticles__8.png and b/port/assets/hud/hud_reticles__8.png differ
diff --git a/port/assets/hud/layout.json b/port/assets/hud/layout.json
index 06c590e7..073a5f74 100644
--- a/port/assets/hud/layout.json
+++ b/port/assets/hud/layout.json
@@ -1203,20 +1203,20 @@
66,
66
],
- "svg": "hud__bitmaps__combined__hud_reticles__0.svg",
- "source_scale": 4,
+ "svg": "hud2_left_reticles__reticle_20.svg",
+ "source_scale": 1.32252,
"source": [
- 0,
- 0
+ -4.1329,
+ -4.1329
],
"clip": [
0,
0,
- 264,
- 264
+ 78,
+ 78
],
"kind": "alpha",
- "score": 0.627
+ "score": 0.995
},
{
"xbox": [
@@ -1225,20 +1225,20 @@
18,
76
],
- "svg": "hud__bitmaps__combined__hud_reticles__0.svg",
- "source_scale": 4,
+ "svg": "hud2_left_reticles__reticle_30.svg",
+ "source_scale": 1.2325,
"source": [
- 0,
- 264
+ 1.6947,
+ 1.3866
],
"clip": [
0,
- 264,
- 72,
- 304
+ 0,
+ 26,
+ 10
],
"kind": "alpha",
- "score": 0.486
+ "score": 0.991
},
{
"xbox": [
@@ -1247,20 +1247,20 @@
94,
28
],
- "svg": "hud__bitmaps__combined__hud_reticles__0.svg",
- "source_scale": 4,
+ "svg": "hud2_left_reticles__reticle_19.svg",
+ "source_scale": 1.35,
"source": [
- 264,
- 0
+ 0.675,
+ 0.675
],
"clip": [
- 264,
0,
- 376,
- 112
+ 0,
+ 38,
+ 38
],
"kind": "alpha",
- "score": 0.547
+ "score": 0.988
},
{
"xbox": [
@@ -1269,20 +1269,20 @@
122,
28
],
- "svg": "hud__bitmaps__combined__hud_reticles__0.svg",
- "source_scale": 4,
+ "svg": "hud2_left_reticles__reticle_28.svg",
+ "source_scale": 1.32812,
"source": [
- 376,
- 0
+ -11.2891,
+ -11.2891
],
"clip": [
- 376,
0,
- 488,
- 112
+ 0,
+ 14,
+ 14
],
"kind": "alpha",
- "score": 0.548
+ "score": 1.0
}
]
},
@@ -1303,20 +1303,20 @@
64,
64
],
- "svg": "hud__bitmaps__combined__hud_reticles__1.svg",
- "source_scale": 4,
+ "svg": "hud2_left_reticles__reticle_17.svg",
+ "source_scale": 1.35947,
"source": [
- 0,
- 0
+ -10.7058,
+ -12.915
],
"clip": [
0,
0,
- 256,
- 256
+ 67,
+ 51
],
"kind": "alpha",
- "score": 0.741
+ "score": 0.998
}
]
},
@@ -1371,20 +1371,20 @@
64,
64
],
- "svg": "hud__bitmaps__combined__hud_reticles__3.svg",
- "source_scale": 4,
+ "svg": "hud2_left_reticles__reticle_06.svg",
+ "source_scale": 1.33099,
"source": [
- 2,
- 1
+ -5.324,
+ -2.9947
],
"clip": [
0,
0,
- 256,
- 256
+ 74,
+ 65
],
"kind": "alpha",
- "score": 0.752
+ "score": 0.987
}
]
},
@@ -1405,20 +1405,20 @@
64,
64
],
- "svg": "hud__bitmaps__combined__hud_reticles__4.svg",
- "source_scale": 4,
+ "svg": "hud2_left_reticles__reticle_07.svg",
+ "source_scale": 1.33636,
"source": [
- -2,
- -2
+ -11.3591,
+ -11.5261
],
"clip": [
0,
0,
- 256,
- 256
+ 64,
+ 64
],
"kind": "alpha",
- "score": 0.753
+ "score": 0.996
}
]
},
@@ -1439,20 +1439,20 @@
64,
64
],
- "svg": "hud__bitmaps__combined__hud_reticles__5.svg",
- "source_scale": 4,
+ "svg": "hud2_left_reticles__reticle_14.svg",
+ "source_scale": 1.3687,
"source": [
- 0,
- 0
+ -4.7905,
+ -6.1592
],
"clip": [
0,
0,
- 256,
- 256
+ 77,
+ 77
],
"kind": "alpha",
- "score": 0.446
+ "score": 0.998
}
]
},
@@ -1473,20 +1473,20 @@
64,
64
],
- "svg": "hud__bitmaps__combined__hud_reticles__6.svg",
- "source_scale": 4,
+ "svg": "hud2_left_reticles__reticle_08.svg",
+ "source_scale": 1.35078,
"source": [
- 0,
- 0
+ -3.7146,
+ -30.0549
],
"clip": [
0,
0,
- 256,
- 256
+ 80,
+ 28
],
"kind": "alpha",
- "score": 0.68
+ "score": 0.985
}
]
},
@@ -1507,20 +1507,20 @@
64,
64
],
- "svg": "hud__bitmaps__combined__hud_reticles__8.svg",
- "source_scale": 4,
+ "svg": "hud2_left_reticles__reticle_23.svg",
+ "source_scale": 1.33389,
"source": [
- 0,
- 0
+ -3.3347,
+ -3.3347
],
"clip": [
0,
0,
- 256,
- 256
+ 78,
+ 78
],
"kind": "alpha",
- "score": 0.46
+ "score": 0.992
}
]
},
@@ -1575,20 +1575,20 @@
64,
64
],
- "svg": "hud__bitmaps__combined__hud_reticles__10.svg",
- "source_scale": 4,
+ "svg": "hud2_left_reticles__reticle_25.svg",
+ "source_scale": 1.37061,
"source": [
- 1,
- 0
+ -7.881,
+ -3.4265
],
"clip": [
0,
0,
- 256,
- 256
+ 71,
+ 79
],
"kind": "alpha",
- "score": 0.763
+ "score": 0.975
}
]
},
@@ -1609,20 +1609,54 @@
64,
64
],
- "svg": "hud__bitmaps__combined__hud_reticles__11.svg",
- "source_scale": 4,
+ "svg": "hud2_left_reticles__reticle_16.svg",
+ "source_scale": 1.36027,
"source": [
- 1,
- -1
+ -3.4007,
+ -11.5623
],
"clip": [
0,
0,
- 256,
- 256
+ 80,
+ 49
],
"kind": "alpha",
- "score": 0.69
+ "score": 0.996
+ }
+ ]
+ },
+ {
+ "name": "hud_reticles__7",
+ "tag": "ui\\hud\\bitmaps\\combined\\hud_reticles",
+ "bitmap": 7,
+ "width": 64,
+ "height": 64,
+ "format": "ay8",
+ "scale": 8,
+ "crc": 1857742423,
+ "cells": [
+ {
+ "xbox": [
+ 0,
+ 0,
+ 64,
+ 64
+ ],
+ "svg": "hud2_left_reticles__reticle_27.svg",
+ "source_scale": 1.34157,
+ "source": [
+ 1.8447,
+ 1.0062
+ ],
+ "clip": [
+ 0,
+ 0,
+ 89,
+ 89
+ ],
+ "kind": "alpha",
+ "score": 0.97
}
]
},
@@ -3142,6 +3176,84 @@
}
]
},
+ {
+ "name": "sniper__hud_reticles_scope__0",
+ "tag": "ui\\hud\\bitmaps\\sniper\\hud_reticles_scope",
+ "bitmap": 0,
+ "width": 64,
+ "height": 64,
+ "format": "a8y8",
+ "scale": 8,
+ "crc": 2373183232,
+ "cells": [
+ {
+ "xbox": [
+ 0,
+ 0,
+ 31,
+ 24
+ ],
+ "svg": "label__Overpass-900__2x.svg",
+ "source_scale": 5.20673,
+ "source": [
+ -22.7794,
+ -9.7626
+ ],
+ "clip": [
+ 0,
+ 0,
+ 117,
+ 127
+ ],
+ "kind": "alpha",
+ "score": 0.972
+ },
+ {
+ "xbox": [
+ 0,
+ 24,
+ 43,
+ 48
+ ],
+ "svg": "label__Overpass-900__10x.svg",
+ "source_scale": 5.28894,
+ "source": [
+ -36.3615,
+ -11.239
+ ],
+ "clip": [
+ 0,
+ 0,
+ 171,
+ 127
+ ],
+ "kind": "alpha",
+ "score": 0.937
+ },
+ {
+ "xbox": [
+ 32,
+ 0,
+ 50,
+ 18
+ ],
+ "svg": "hud2_left_reticles__reticle_18.svg",
+ "source_scale": 1.365,
+ "source": [
+ -3.5831,
+ -3.5831
+ ],
+ "clip": [
+ 0,
+ 0,
+ 16,
+ 16
+ ],
+ "kind": "alpha",
+ "score": 0.991
+ }
+ ]
+ },
{
"name": "sniper_rifle__angle_ticks__0",
"tag": "weapons\\sniper rifle\\bitmaps\\angle_ticks",
diff --git a/port/assets/hud/sniper__hud_reticles_scope__0.png b/port/assets/hud/sniper__hud_reticles_scope__0.png
new file mode 100644
index 00000000..754a8821
Binary files /dev/null and b/port/assets/hud/sniper__hud_reticles_scope__0.png differ
diff --git a/port/assets/hud/svg/hud2_left_reticles__reticle_06.svg b/port/assets/hud/svg/hud2_left_reticles__reticle_06.svg
new file mode 100644
index 00000000..2d87ea68
--- /dev/null
+++ b/port/assets/hud/svg/hud2_left_reticles__reticle_06.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/port/assets/hud/svg/hud2_left_reticles__reticle_07.svg b/port/assets/hud/svg/hud2_left_reticles__reticle_07.svg
new file mode 100644
index 00000000..ff407d67
--- /dev/null
+++ b/port/assets/hud/svg/hud2_left_reticles__reticle_07.svg
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/port/assets/hud/svg/hud2_left_reticles__reticle_08.svg b/port/assets/hud/svg/hud2_left_reticles__reticle_08.svg
new file mode 100644
index 00000000..8befadc4
--- /dev/null
+++ b/port/assets/hud/svg/hud2_left_reticles__reticle_08.svg
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/port/assets/hud/svg/hud2_left_reticles__reticle_14.svg b/port/assets/hud/svg/hud2_left_reticles__reticle_14.svg
new file mode 100644
index 00000000..3a166826
--- /dev/null
+++ b/port/assets/hud/svg/hud2_left_reticles__reticle_14.svg
@@ -0,0 +1,33 @@
+
+
\ No newline at end of file
diff --git a/port/assets/hud/svg/hud2_left_reticles__reticle_16.svg b/port/assets/hud/svg/hud2_left_reticles__reticle_16.svg
new file mode 100644
index 00000000..b4c31580
--- /dev/null
+++ b/port/assets/hud/svg/hud2_left_reticles__reticle_16.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/port/assets/hud/svg/hud2_left_reticles__reticle_17.svg b/port/assets/hud/svg/hud2_left_reticles__reticle_17.svg
new file mode 100644
index 00000000..0fce0fba
--- /dev/null
+++ b/port/assets/hud/svg/hud2_left_reticles__reticle_17.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/port/assets/hud/svg/hud2_left_reticles__reticle_18.svg b/port/assets/hud/svg/hud2_left_reticles__reticle_18.svg
new file mode 100644
index 00000000..9ea19d90
--- /dev/null
+++ b/port/assets/hud/svg/hud2_left_reticles__reticle_18.svg
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/port/assets/hud/svg/hud2_left_reticles__reticle_19.svg b/port/assets/hud/svg/hud2_left_reticles__reticle_19.svg
new file mode 100644
index 00000000..db03bf4c
--- /dev/null
+++ b/port/assets/hud/svg/hud2_left_reticles__reticle_19.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/port/assets/hud/svg/hud2_left_reticles__reticle_20.svg b/port/assets/hud/svg/hud2_left_reticles__reticle_20.svg
new file mode 100644
index 00000000..58b54666
--- /dev/null
+++ b/port/assets/hud/svg/hud2_left_reticles__reticle_20.svg
@@ -0,0 +1,17 @@
+
+
\ No newline at end of file
diff --git a/port/assets/hud/svg/hud2_left_reticles__reticle_23.svg b/port/assets/hud/svg/hud2_left_reticles__reticle_23.svg
new file mode 100644
index 00000000..91740be8
--- /dev/null
+++ b/port/assets/hud/svg/hud2_left_reticles__reticle_23.svg
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/port/assets/hud/svg/hud2_left_reticles__reticle_25.svg b/port/assets/hud/svg/hud2_left_reticles__reticle_25.svg
new file mode 100644
index 00000000..87c969dd
--- /dev/null
+++ b/port/assets/hud/svg/hud2_left_reticles__reticle_25.svg
@@ -0,0 +1,25 @@
+
+
\ No newline at end of file
diff --git a/port/assets/hud/svg/hud2_left_reticles__reticle_27.svg b/port/assets/hud/svg/hud2_left_reticles__reticle_27.svg
new file mode 100644
index 00000000..5d15d61c
--- /dev/null
+++ b/port/assets/hud/svg/hud2_left_reticles__reticle_27.svg
@@ -0,0 +1,29 @@
+
+
\ No newline at end of file
diff --git a/port/assets/hud/svg/hud2_left_reticles__reticle_28.svg b/port/assets/hud/svg/hud2_left_reticles__reticle_28.svg
new file mode 100644
index 00000000..dcfcf840
--- /dev/null
+++ b/port/assets/hud/svg/hud2_left_reticles__reticle_28.svg
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/port/assets/hud/svg/hud2_left_reticles__reticle_30.svg b/port/assets/hud/svg/hud2_left_reticles__reticle_30.svg
new file mode 100644
index 00000000..5e7cefe8
--- /dev/null
+++ b/port/assets/hud/svg/hud2_left_reticles__reticle_30.svg
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__0.svg b/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__0.svg
deleted file mode 100644
index c7170dd0..00000000
--- a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__0.svg
+++ /dev/null
@@ -1,29 +0,0 @@
-
diff --git a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__1.svg b/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__1.svg
deleted file mode 100644
index 29b458f8..00000000
--- a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__1.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
diff --git a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__10.svg b/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__10.svg
deleted file mode 100644
index d168f985..00000000
--- a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__10.svg
+++ /dev/null
@@ -1,24 +0,0 @@
-
diff --git a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__11.svg b/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__11.svg
deleted file mode 100644
index 11e0459e..00000000
--- a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__11.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__3.svg b/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__3.svg
deleted file mode 100644
index 87dfad68..00000000
--- a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__3.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
diff --git a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__4.svg b/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__4.svg
deleted file mode 100644
index 575754bd..00000000
--- a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__4.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__5.svg b/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__5.svg
deleted file mode 100644
index d0de3500..00000000
--- a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__5.svg
+++ /dev/null
@@ -1,29 +0,0 @@
-
diff --git a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__6.svg b/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__6.svg
deleted file mode 100644
index 8a8a27e3..00000000
--- a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__6.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
diff --git a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__8.svg b/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__8.svg
deleted file mode 100644
index caa6c550..00000000
--- a/port/assets/hud/svg/hud__bitmaps__combined__hud_reticles__8.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/port/assets/hud/svg/label__Overpass-900__10x.svg b/port/assets/hud/svg/label__Overpass-900__10x.svg
new file mode 100644
index 00000000..9ab5cfc1
--- /dev/null
+++ b/port/assets/hud/svg/label__Overpass-900__10x.svg
@@ -0,0 +1,8 @@
+
diff --git a/port/assets/hud/svg/label__Overpass-900__2x.svg b/port/assets/hud/svg/label__Overpass-900__2x.svg
new file mode 100644
index 00000000..3cbd1344
--- /dev/null
+++ b/port/assets/hud/svg/label__Overpass-900__2x.svg
@@ -0,0 +1,7 @@
+
diff --git a/port/linux/README.md b/port/linux/README.md
index 48c35f54..34e28bd0 100644
--- a/port/linux/README.md
+++ b/port/linux/README.md
@@ -941,7 +941,7 @@ crafted file) is refused, and the level starts over.
| --- | --- |
| 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 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 (the weapons' reticles from those of the PC HUD sheet that match the Xbox's exactly, the sniper rifle's 2x and 10x set in Overpass), 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 resolution the game draws at, 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. |
| Anti-aliasing | Off unless `display.anti_aliasing` is set (`src/d3d8_gl.c`, `src/xgpu_post.c`). FXAA (written in the port) and SMAA (`port/third_party/smaa`, MIT licensed, at its HIGH preset, compiled as GLSL) are passes over the 3D view of each window, after the lens flares and before the HUD and the menus (`render/render.c`). Their programs are built when the setting is chosen. Supersampling draws the render targets the size of the screen at two times the resolution in each direction, and the display blit scales them down. Multisampling draws the back buffer and its depth buffer into multisampled renderbuffers, and with them any target that is drawn together with one of them (a mirror's view, in the secondary target with the back buffer's depth buffer), so that the attachments of a framebuffer are all multisampled or none is. A target's pixels are resolved into its texture before something reads the texture (as a texture, or at the display blit). Visibility tests count samples, divided by the samples of a pixel. An alpha-tested surface (foliage, grates) covers the samples of a pixel in proportion to its alpha past the reference (`gl_SampleMask`, not on Android). |
| 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. |
diff --git a/port/linux/src/hud_hires.c b/port/linux/src/hud_hires.c
index 5ccfd226..f2e420ed 100644
--- a/port/linux/src/hud_hires.c
+++ b/port/linux/src/hud_hires.c
@@ -6,7 +6,7 @@ uploaded, and each one's GL texture.
Which bitmap is at an address the game knows (from the loaded map's tags:
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
+first drawn and kept: up to 71 of the HUD's, about 227 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), and their button icons, about 0.3 MB each (5.3
diff --git a/tools/hud_assets.py b/tools/hud_assets.py
index b1a7fb9a..6a8381b6 100644
--- a/tools/hud_assets.py
+++ b/tools/hud_assets.py
@@ -24,6 +24,16 @@ layout: reads the Xbox map's HUD bitmaps (sizes, formats, sprite rectangles,
(numbers only) with copies of the redraws used in port/assets/hud/svg.
Needs a map and the restored tags; its output is committed. A bitmap
with a cell that no redraw matches is left out.
+reticles: puts the reticle redraws that match the Xbox's exactly (RETICLES,
+ from the PC HUD sheet's own drawing, at its scale) in the cells of the
+ Xbox sequences they draw, in place of what layout chose: each copied
+ unchanged into port/assets/hud/svg, at the size and place that match the
+ Xbox sprite best. The sniper's zoom labels beside its reticle, which no
+ redraw has, are set in a font (LABELS). Updates layout.json's reticle
+ entries only; run it after layout, which puts back the reticles it chose.
+
+ python tools/hud_assets.py reticles --map assets/maps/bloodgulch.map \\
+ --reticles ../halo-pc-restored/HUD2_left_reticles
build: renders port/assets/hud/svg with layout.json into port/assets/hud/*.png
(committed; the builds embed them).
check: compares each PNG, reduced to the tag's size, with the map's bitmap
@@ -112,6 +122,42 @@ KINDS = {0: "a8", 1: "y8", 2: "alpha", 3: "grey", 6: "colour", 8: "colour", 9: "
SEARCH = 4
MATCH = 0.25
+# the reticle redraws that match the Xbox's exactly (the same design, only
+# the strokes' weight differs), by the sheet and sequence they draw: cut from
+# the PC HUD sheet's drawing, each at that drawing's own scale
+RETICLES = {
+ HUD + "combined\\hud_reticles": {
+ 0: "reticle_20.svg", # assault rifle
+ 1: "reticle_17.svg", # banshee gun
+ 3: "reticle_06.svg", # plasma pistol
+ 4: "reticle_07.svg", # plasma rifle
+ 5: "reticle_14.svg", # (no weapon's; probably the flamethrower's)
+ 6: "reticle_08.svg", # needler
+ 7: "reticle_28.svg", # no weapon
+ 8: "reticle_19.svg", # pistol
+ 9: "reticle_27.svg", # rocket launcher, wraith mortar
+ 10: "reticle_23.svg", # shotgun
+ 12: "reticle_25.svg", # covenant gun turret
+ 13: "reticle_16.svg", # ghost gun
+ 14: "reticle_30.svg", # (no weapon's; a small chevron)
+ },
+ HUD + "sniper\\hud_reticles_scope": {
+ 0: "reticle_18.svg", # sniper rifle
+ },
+}
+# the zoom labels beside the sniper's reticle, by sequence and sprite: no
+# redraw has the Xbox's (the PC's say 2x and 8x), so they are set in a font
+# that matches them, with the spacing between their glyphs that fits best
+LABELS = {
+ HUD + "sniper\\hud_reticles_scope": {(1, 0): "2x", (1, 1): "10x"},
+}
+LABEL_FONT = Path("port/assets/fonts/Overpass-900.ttf")
+LABEL_SPACINGS = (-10, -8, -6, -4, -2, 0, 2, 4)
+# render texels per Xbox texel while fitting one, and the scales tried
+# around the one its extent gives
+FIT_TEXELS = 8
+FIT_SCALES = np.linspace(0.85, 1.15, 31)
+
# ---------- Xbox maps
@@ -456,15 +502,15 @@ def layout(arguments) -> None:
kind = "meter"
flat = svg.replace("/", "__").replace(" ", "_")
sources[flat] = svg
- match = {"xbox": cell, "svg": flat, "source_scale": scale,
- "source": corner, "clip": clip, "kind": kind, "score": round(score, 3)}
+ laid = {"xbox": cell, "svg": flat, "source_scale": scale,
+ "source": corner, "clip": clip, "kind": kind, "score": round(score, 3)}
if tag == HUD + "combined\\hud_unit_meters":
original = xbox[top:bottom, left:right]
values = set(np.unique(original[..., 2][original[..., 3] > 0]))
health = set(range(30, 241, 30))
if health <= values <= health | {0}:
- match["thresholds"] = sorted(health)
- matched.append(match)
+ laid["thresholds"] = sorted(health)
+ matched.append(laid)
if not matched:
if matched is not None:
print(f"{name}: left out, empty")
@@ -495,6 +541,172 @@ def layout(arguments) -> None:
LAYOUT.write_text(json.dumps({"assets": entries}, indent=1) + "\n")
+# ---------- the reticles
+
+
+def soften(image: np.ndarray) -> np.ndarray:
+ """A shape with its edges softened, so that a thinner stroke on one side
+ still lines up with the other's (the Xbox's are heavier and blurred)."""
+ from scipy import ndimage
+
+ return ndimage.gaussian_filter(image.astype(float), 0.8)
+
+
+def likeness(a: np.ndarray, b: np.ndarray) -> float:
+ a, b = a - a.mean(), b - b.mean()
+ norm = np.sqrt((a * a).sum() * (b * b).sum())
+ return float((a * b).sum() / norm) if norm else 0.0
+
+
+def extent(alpha: np.ndarray) -> tuple:
+ """The box (left, top, right, bottom) of a shape's clearly covered texels."""
+ ys, xs = np.nonzero(alpha >= alpha.max() / 4)
+ return xs.min(), ys.min(), xs.max() + 1, ys.max() + 1
+
+
+def text_svg(font: Path, text: str, spacing: float = 0) -> str:
+ """text's outlines in a font, as an SVG (an em of 100 units), for a
+ label that has no redraw: the glyphs as the font draws them, set with
+ its advances and spacing (in hundredths of an em) between them."""
+ from fontTools.pens.svgPathPen import SVGPathPen
+ from fontTools.ttLib import TTFont
+
+ face = TTFont(str(font))
+ glyphs = face.getGlyphSet()
+ cmap = face.getBestCmap()
+ scale = 100 / face["head"].unitsPerEm
+ paths, x = [], 0
+ for index, character in enumerate(text):
+ name = cmap[ord(character)]
+ pen = SVGPathPen(glyphs)
+ glyphs[name].draw(pen)
+ paths.append(f'')
+ x += face["hmtx"][name][0] + (spacing / scale if index + 1 < len(text) else 0)
+ ascent, descent = face["hhea"].ascent, face["hhea"].descent
+ width, height = x * scale, (ascent - descent) * scale
+ return (f'\n")
+
+
+def fit_reticle(svg: Path, alpha: np.ndarray) -> tuple:
+ """The size and place at which a redraw best matches an Xbox cell's
+ alpha: (likeness, redraw units per Xbox texel, the redraw's point at the
+ cell's corner)."""
+ height, width = alpha.shape
+ target = soften(alpha / 255.0)
+
+ def drawn(zoom):
+ return render_svg(svg, zoom)[..., 3].astype(float) / 255
+
+ base = drawn(FIT_TEXELS)
+ svg_box = [value / FIT_TEXELS for value in extent(base)]
+ xbox_box = extent(alpha)
+ # (the Xbox's extent with its blur's texel taken off each side)
+ units = max((svg_box[2] - svg_box[0]) / max(xbox_box[2] - xbox_box[0] - 1, 1),
+ (svg_box[3] - svg_box[1]) / max(xbox_box[3] - xbox_box[1] - 1, 1))
+ best = (-2.0, units, [0.0, 0.0])
+ for factor in FIT_SCALES:
+ per_texel = units * factor
+ zoom = FIT_TEXELS / per_texel
+ render = drawn(zoom)
+ box = extent(render)
+ # (centred on the Xbox shape's centre, then moved by eighths of a texel)
+ centre = ((box[0] + box[2]) / 2 - (xbox_box[0] + xbox_box[2]) / 2 * FIT_TEXELS,
+ (box[1] + box[3]) / 2 - (xbox_box[1] + xbox_box[3]) / 2 * FIT_TEXELS)
+ for dy in range(-FIT_TEXELS * 3 // 2, FIT_TEXELS * 3 // 2 + 1):
+ for dx in range(-FIT_TEXELS * 3 // 2, FIT_TEXELS * 3 // 2 + 1):
+ at = (round(centre[0]) + dx, round(centre[1]) + dy)
+ reduced = window(render, at[0], at[1], width * FIT_TEXELS, height * FIT_TEXELS).reshape(
+ height, FIT_TEXELS, width, FIT_TEXELS).mean(axis=(1, 3))
+ score = likeness(soften(reduced), target)
+ if score > best[0]:
+ best = (score, per_texel, [at[0] / zoom, at[1] / zoom])
+ return best
+
+
+def reticles(arguments) -> None:
+ xbox_map = XboxMap(Path(arguments.map))
+ source = Path(arguments.reticles)
+ description = json.loads(LAYOUT.read_text())
+ entries = description["assets"]
+ for tag, redraws in RETICLES.items():
+ group = xbox_map.bitmap_group(tag)
+ labels = LABELS.get(tag, {})
+ for index, bitmap in enumerate(group["bitmaps"]):
+ width, height = bitmap["width"], bitmap["height"]
+ xbox = decode_bitmap(bitmap)
+ # (named as layout names them)
+ name = tag[len(HUD):].replace("combined\\", "").replace("\\", "__").replace(" ", "_") + f"__{index}"
+ old = next((entry for entry in entries if entry["name"] == name), None)
+ cells = {}
+ for sequence_index, sequence in enumerate(group["sequences"]):
+ for sprite_index, sprite in enumerate(sequence):
+ if sprite[0] == index:
+ cells.setdefault(tuple(pixel_rectangle(sprite, width, height)), set()).add(
+ (sequence_index, sprite_index))
+ laid = []
+ for cell, numbers in sorted(cells.items()):
+ left, top, right, bottom = cell
+ alpha = xbox[top:bottom, left:right, 3].astype(float)
+ exact = sorted(redraws[sequence] for sequence, _ in numbers if sequence in redraws)
+ text = next((labels[number] for number in sorted(numbers) if number in labels), None)
+ if exact:
+ svg = "hud2_left_reticles__" + exact[0]
+ shutil.copyfile(source / exact[0], ASSETS / "svg" / svg)
+ score, per_texel, corner = fit_reticle(ASSETS / "svg" / svg, alpha)
+ what = exact[0]
+ elif text:
+ best = None
+ for spacing in LABEL_SPACINGS:
+ candidate = ASSETS / "svg" / f"label__{LABEL_FONT.stem}__{text}.svg"
+ candidate.write_text(text_svg(LABEL_FONT, text, spacing))
+ fit = fit_reticle(candidate, alpha)
+ if best is None or fit[0] > best[0][0]:
+ best = (fit, spacing)
+ (score, per_texel, corner), spacing = best
+ svg = f"label__{LABEL_FONT.stem}__{text}.svg"
+ (ASSETS / "svg" / svg).write_text(text_svg(LABEL_FONT, text, spacing))
+ what = f'"{text}" in {LABEL_FONT.name}, spacing {spacing}'
+ else:
+ kept = next((c for c in (old or {"cells": []})["cells"] if c["xbox"] == list(cell)), None)
+ if kept is None and alpha.any():
+ print(f"{name}: left as it was, cell {list(cell)} has no redraw")
+ laid = None
+ break
+ if kept is not None:
+ laid.append(kept)
+ continue
+ size = render_svg(ASSETS / "svg" / svg)[..., 3].shape
+ laid.append({"xbox": list(cell), "svg": svg, "source_scale": round(per_texel, 5),
+ "source": [round(corner[0], 4), round(corner[1], 4)],
+ "clip": [0, 0, size[1], size[0]], "kind": "alpha", "score": round(score, 3)})
+ print(f"{name} {list(cell)} (sequence, sprite {sorted(numbers)}): {what} at "
+ f"{per_texel:.4f} units a texel, likeness {score:.3f}")
+ if not laid or all(not c["svg"].startswith(("hud2_left_reticles__", "label__")) for c in laid):
+ continue
+ entry = {"name": name, "tag": tag, "bitmap": index, "width": width, "height": height,
+ "format": FORMATS[bitmap["format"]], "scale": SCALE,
+ "crc": zlib.crc32(bitmap["pixels"][:level0_size(bitmap)]), "cells": laid}
+ if old is not None:
+ entries[entries.index(old)] = entry
+ else:
+ # (after the sheet's other bitmaps, or its folder's)
+ folder = tag.rsplit("\\", 1)[0]
+ position = max((i for i, e in enumerate(entries) if e["tag"] == tag), default=None)
+ if position is None:
+ position = max(i for i, e in enumerate(entries) if e["tag"].startswith(folder))
+ entries.insert(position + 1, entry)
+ used = {cell["svg"] for entry in entries for cell in entry["cells"]}
+ for svg in (ASSETS / "svg").glob("*.svg"):
+ if svg.name not in used:
+ svg.unlink()
+ print(f"{svg.name}: no longer used")
+ LAYOUT.write_text(json.dumps(description, indent=1) + "\n")
+
+
# ---------- building
@@ -605,16 +817,19 @@ def build_asset(entry: dict, renders: dict) -> np.ndarray:
scale = entry["scale"]
image = np.zeros((entry["height"] * scale, entry["width"] * scale, 4), np.uint8)
for cell in entry["cells"]:
- # (the redraw drawn at the texture's scale, its coordinates with it)
- zoom = scale // cell["source_scale"]
- assert zoom * cell["source_scale"] == scale, entry["name"]
+ # (the redraw drawn at the texture's scale, its coordinates with it:
+ # a whole multiple of the redraw's for the PC sheets', any for the
+ # reticles')
+ zoom = scale / cell["source_scale"]
+ if zoom == int(zoom):
+ zoom = int(zoom)
key = (cell["svg"], zoom)
if key not in renders:
renders[key] = render_svg(ASSETS / "svg" / cell["svg"], zoom)
left, top, right, bottom = cell["xbox"]
width, height = (right - left) * scale, (bottom - top) * scale
- texels = window(clipped(renders[key], [value * zoom for value in cell["clip"]]),
- cell["source"][0] * zoom, cell["source"][1] * zoom, width, height)
+ texels = window(clipped(renders[key], [round(value * zoom) for value in cell["clip"]]),
+ round(cell["source"][0] * zoom), round(cell["source"][1] * zoom), width, height)
image[top * scale:top * scale + height, left * scale:left * scale + width] = recipe(texels, cell["kind"])
if any(cell["kind"] in ("meter", "dxt") for cell in entry["cells"]):
image = bleed(image)
@@ -682,13 +897,17 @@ def main() -> None:
command.add_argument("--map", required=True)
command.add_argument("--hek", required=True)
command.add_argument("--svg", required=True)
+ command = commands.add_parser("reticles")
+ command.add_argument("--map", required=True)
+ command.add_argument("--reticles", required=True)
commands.add_parser("build")
commands.add_parser("thresholds")
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, "thresholds": thresholds, "check": check}[arguments.command](arguments)
+ {"layout": layout, "reticles": reticles, "build": build, "thresholds": thresholds,
+ "check": check}[arguments.command](arguments)
if __name__ == "__main__":