mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-23 15:52:42 +02:00
Deck/Windows playtest fixes: fog rework (D540) and RSP-style CPU near-plane clip with per-vertex N64 fog (D543), with fog recomputed on near-plane-clipped vertices as the RSP does, so the haze around the player stays steady (D553); colour-combiner clamp for banded walls (D548); rocket and thrown-item stack overflows (D545, D549); menu double-trigger mitigation (D541); Dam ending first-shot camera swivel restored by removing an old port look-at workaround (D552); settings standardisation (D546), live All unlocked (D547), F10 mouse (D544); static window title and the project mark (the in-game crosshair shape, site gold) as window/taskbar icon and favicon (D550). Reference frames re-based on Windows, Linux and Steam Deck (A/B-confirmed); capture_p7.sh runs on Windows. Opt-in update check, off by default (D551: one HTTPS request to GitHub per launch when enabled; winhttp.dll loaded only then; the tag CI job checks the tag against GE007_VERSION). Release notes, ROADMAP, internals and security docs updated; tree-hygiene pass. Refs: D540 D541 D543 D544 D545 D546 D547 D548 D549 D550 D551 D552 D553 D554 D555 D556 D557 D558 D559 D560 D561 D562 D563 D564
214 lines
8.5 KiB
Python
214 lines
8.5 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)
|
|
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"
|
|
|
|
# --- 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)
|
|
|
|
|
|
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_HEADER.relative_to(ROOT)}")
|
|
return 0
|
|
stale = [p for p in (OUT_ICON, OUT_ICO, OUT_HEADER) 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())
|