forked from Ports/goldeneye-pc-port
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.
218 lines
8.7 KiB
Python
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())
|