Files
goldeneye-pc-port/tools_pc/make_icon.py
T
James Dansereau 1d15e5695a v0.5.0 final pass: AI and Deck fixes (D565-D567), docs and site refresh
Fixes:
- D565: AI awareness no longer follows two player render settings. Fog
  distance is floored at 100 (below it the shortened clip hid guards from
  AI on-screen checks); the D466 gameplay fog cull is rescaled to the game's
  own FOV (identity at defaults).
- D566: Steam Deck Game Mode no longer shows the on-screen keyboard at
  launch (SDL text input stopped; screen-keyboard hint off).
- D567: opening PC Options from the front end with a pad no longer drags
  the game's crosshair to the top-left corner.

Docs and site:
- README News/Status/Features/controls brought up to date; the unpublished
  v0.4.1 work is folded into v0.5.0 everywhere; stats to v0.5.0.
- Release notes reordered: player-facing highlights first, finding labels
  and verification at the bottom.
- Site status rewritten; favicon is now the exe icon (make_icon.py writes
  docs/favicon.ico).
- ROADMAP: AI review results, update-check follow-up, stale rows closed.

Verified: Windows + Linux builds; Windows pixel gate Surface 1 PASS;
Deck build deployed for the maintainer's Game Mode checks.
2026-10-06 21:06:48 -04:00

218 lines
8.7 KiB
Python

#!/usr/bin/env python3
"""Generate the project's own icon (original art).
The mark is an ORIGINAL generic version of the GE default aim crosshair:
a circle with four lines (N/S/E/W) crossing the circle's edge and
extending well past it, open at the centre. Proportions are measured from
the game's own default crosshair (thin ring, long bars, open centre).
Site gold (#e8a13a) on near-black (#050607).
A circle-plus-ticks reticle is a generic functional shape (no logo art,
wordmark or screenshot copied, cf. tree-hygiene rule 4: repo imagery
must not be derivative game content).
Outputs (all regenerated byte-identically; deterministic):
docs/img/icon/ge007-icon.png 512x512 favicon / README icon
cmake/icon/ge007.ico multi-size Windows exe icon (16..256)
docs/favicon.ico byte copy of ge007.ico (site favicon)
port/fast3d/ge007_icon.h 32x32 RGBA array for the SDL window icon
(SDL_SetWindowIcon, see gfx_sdl2.cpp)
Usage:
python tools_pc/make_icon.py # write all three
python tools_pc/make_icon.py --check # exit 1 if any output is missing,
# or the mark is not exactly centred
Requires Pillow (>=10.1, for load_default(size)).
"""
from __future__ import annotations
import io
import struct
import sys
from pathlib import Path
try:
from PIL import Image, ImageDraw
except ImportError:
sys.exit("Pillow is required: pip install Pillow")
ROOT = Path(__file__).resolve().parent.parent
OUT_ICON = ROOT / "docs" / "img" / "icon" / "ge007-icon.png"
OUT_ICO = ROOT / "cmake" / "icon" / "ge007.ico"
OUT_HEADER = ROOT / "port" / "fast3d" / "ge007_icon.h"
OUT_FAVICON = ROOT / "docs" / "favicon.ico"
# --- palette -----------------------------------------------------------------
BG = (5, 6, 7) # site near-black
GOLD = (232, 161, 58) # site gold (#e8a13a)
def _crosshair(size: int, small: bool = False) -> Image.Image:
"""The generic GE default crosshair: a circle with N/S/E/W lines
crossing its edge and extending well past it, open centre, in site gold
(#e8a13a); proportions measured from the game's own default crosshair
(thin ring, long bars, open centre). Rendered at 2x for AA, then
downsized. small: a chunkier variant (thicker strokes, wider gap) so
the mark survives 16px in the .ico small slots (and the 32px window
icon)."""
S = size * 2
img = Image.new("RGBA", (S, S), BG + (255,))
dr = ImageDraw.Draw(img, "RGBA")
# Exact centre = the line between the two middle supersample pixels
# (continuous coordinate S/2; pixel i covers [i, i+1)). Every shape is
# drawn symmetric about it with inclusive PIL coordinates, so each size
# is pixel-exactly mirror-symmetric (checked by _symmetric()).
c = S // 2
def rr(v: float) -> int:
return int(round(S * v / 512))
def even(v: float) -> int:
return max(2, 2 * int(round(S * v / 512 / 2)))
def disc(r: int) -> list:
return [c - r, c - r, c + r - 1, c + r - 1]
# faint gold glow behind the reticle (kept very subtle: flat + calm)
glow = Image.new("RGBA", (S, S), (0, 0, 0, 0))
gd = ImageDraw.Draw(glow)
gr = rr(190 if small else 200)
for i in range(24, 0, -1):
g = gr * i // 24
if g > 0:
gd.ellipse(disc(g), fill=GOLD + (4,))
img.alpha_composite(glow)
# the reticle: a circle + four bars (N/S/E/W) crossing the circle's
# edge and extending well past it, with an open centre -- proportions
# measured off the game's own default crosshair (thin ring, long bars;
# bar reach ~1.45x the ring's outer radius, centre gap ~0.30x).
# Small sizes get the chunky variant (thicker strokes, wider gap so
# it stays open at 16px).
circle_r = rr(140 if small else 150) # outer radius of the ring
ring_w = rr(34 if small else 16) # ring stroke width
bar_w = even(30 if small else 14) # bar stroke width (even: symmetric)
reach = rr(205 if small else 218)
gap = rr(52 if small else 45)
hw = bar_w // 2
dr.ellipse(disc(circle_r), outline=GOLD + (255,), width=ring_w)
for a, b in ((c - reach, c - gap - 1), (c + gap, c + reach - 1)):
dr.rectangle([c - hw, a, c + hw - 1, b], fill=GOLD + (255,)) # vertical
dr.rectangle([a, c - hw, b, c + hw - 1], fill=GOLD + (255,)) # horizontal
return img.resize((size, size), Image.LANCZOS)
def _symmetric(img: Image.Image) -> bool:
"""Pixel-exact left-right and top-bottom mirror symmetry: the mark is
exactly centred at this size. (Not the diagonal: LANCZOS resamples the
two axes in a fixed order, so a transpose differs by rounding only.)"""
return (img.tobytes() == img.transpose(Image.FLIP_LEFT_RIGHT).tobytes()
and img.tobytes() == img.transpose(Image.FLIP_TOP_BOTTOM).tobytes())
def _asymmetric_sizes() -> list:
sizes = [(512, False), (16, True), (24, True), (32, True),
(48, False), (64, False), (128, False), (256, False)]
return [f"{s}{' (small)' if sm else ''}" for s, sm in sizes
if not _symmetric(_crosshair(s, small=sm))]
def _ico_png(size: int, small: bool) -> bytes:
buf = io.BytesIO()
_crosshair(size, small=small).save(buf, format="PNG")
return buf.getvalue()
def make_ico() -> None:
_crosshair(512).save(OUT_ICON)
# Multi-size .ico with PNG-compressed entries (Vista+ style; what the
# 256 slot always is). Small sizes get the chunky variant so the mark
# survives 16px; a single 512-master downsample washes out there.
entries = [(_ico_png(s, small=True), s) for s in (16, 24, 32)]
entries += [(_ico_png(s, small=False), s) for s in (48, 64, 128, 256)]
n = len(entries)
offset = 6 + 16 * n
dirs, data = b"", b""
for png, s in entries:
wh = 0 if s == 256 else s # 0 means 256 in the ICO format
dirs += struct.pack("<BBBBHHII", wh, wh, 0, 0, 1, 32,
len(png), offset)
offset += len(png)
data += png
OUT_ICO.write_bytes(struct.pack("<HHH", 0, 1, n) + dirs + data)
OUT_FAVICON.write_bytes(OUT_ICO.read_bytes()) # site favicon = exe icon
def make_header() -> None:
"""Emit the 32x32 RGBA array for the SDL window icon (gfx_sdl2.cpp).
Byte order is plain R,G,B,A per pixel (portable; the C++ side packs
it against explicit masks).
NOTE: this output is C, and CMake compiles it inside gfx_sdl2.cpp's
include — it is NOT independently syntax-checked by this script. Keep
it trivially valid (see the row-brace comment below; a flat cell list
without row braces is ill-formed for a 3-D array)."""
img = _crosshair(32, small=True).convert("RGBA")
px = img.load()
rows = []
for y in range(32):
row = []
for x in range(32):
r, g, b, a = px[x, y]
row.append(f"{{{r:3d},{g:3d},{b:3d},{a:3d}}}")
# Row braces are REQUIRED: without them all 1024 cells sit at the
# top level of the [32][32][4] initializer (1024 > first dim 32 ->
# "too many initializers").
rows.append(" {" + ",".join(row) + "}")
OUT_HEADER.write_text(
"/* Generated by tools_pc/make_icon.py -- do not edit by hand.\n"
" * The project mark (a generic version of the GE default aim\n"
" * crosshair) as a 32x32 RGBA array, row-major, plain R,G,B,A\n"
" * byte order per pixel; used by gfx_sdl2.cpp for\n"
" * SDL_SetWindowIcon. Regenerate: python tools_pc/make_icon.py */\n"
"#ifndef GE007_ICON_H\n"
"#define GE007_ICON_H\n"
"\n"
"#include <stdint.h>\n"
"\n"
"#define GE007_ICON_SIZE 32\n"
"static const uint8_t ge007_icon_rgba[GE007_ICON_SIZE]"
"[GE007_ICON_SIZE][4] = {\n"
+ ",\n".join(rows) + "\n};\n"
"#endif /* GE007_ICON_H */\n"
)
def main() -> int:
args = sys.argv[1:]
check = "--check" in args
for p in (OUT_ICON.parent, OUT_ICO.parent, OUT_HEADER.parent):
p.mkdir(parents=True, exist_ok=True)
bad = _asymmetric_sizes()
if bad:
print("icon is not exactly centred (mirror-asymmetric) at: " + ", ".join(bad))
return 1
if not check:
make_ico()
make_header()
print(f"wrote {OUT_ICON.relative_to(ROOT)}")
print(f"wrote {OUT_ICO.relative_to(ROOT)}")
print(f"wrote {OUT_FAVICON.relative_to(ROOT)}")
print(f"wrote {OUT_HEADER.relative_to(ROOT)}")
return 0
stale = [p for p in (OUT_ICON, OUT_ICO, OUT_HEADER, OUT_FAVICON) if not p.exists()]
if stale:
for p in stale:
print(f"stale/missing: {p.relative_to(ROOT)}")
return 1
print("ok (all icon outputs present and mirror-symmetric at every size; "
"regenerate with: python tools_pc/make_icon.py)")
return 0
if __name__ == "__main__":
sys.exit(main())