EBLAN_ENGINE/tools/make_texturepack.py
Artvasil09 a1d1baa869 feat: complete engine and editor subsystems (Stages 1-5 from TODO_ARTEM)
- Stage 1: Eliminate fish-eye distortion with precomputed tangential ray offset table and fractional DDA depth
- Stage 2: Full asset management in EblanStudio (stb_image/stb_image_write, 128x128 BGRA texture packaging, WAV sound manager)
- Stage 3: Multi-level room streaming, LevelTransition/CELL_PORTAL trigger system, 16-frame screen fade to black
- Stage 4: 60Hz cutscene engine in pure x86 ASM (v4 binary track, waypoint camera motion, shortest-arc pan, CP1251 Russian subtitles, letterbox overlay, audio)
- Stage 5: EblanStudio cutscene timeline (F6) and windowless Live ASM Viewport over shared-memory IPC directly rendered into DX11
- Tooling & Tests: comprehensive regression test suite (94 editor + 107 engine tests), IPC tests, and cutscene demo
2026-10-22 16:24:22 +03:00

255 lines
8.3 KiB
Python

#!/usr/bin/env python3
"""make_texturepack.py - Build assets/textures/texturepack.bin for EBLAN ENGINE.
Pack layout (little-endian):
dd magic 0x58544245 ('EBTX')
dw version (1)
dw slotCount (16)
then slotCount * 65536 bytes: 128x128 BGRA32 pixels per slot, slot 0 first.
DIB memory order note: a DWORD 00RRGGBBh stores bytes B,G,R,0 little-endian.
Sources:
- Slot 1..48 taken from a KAZIK-format pack (48 slots, 128x128 BGRA)
when --kazik path is given (default: ..\\..\\KAZIK\\assets\\textures\\casino3d-hd.bin).
- Missing slots are filled with procedural fallback patterns that mirror
the engine's GenerateDefaultTextures (with correct colors).
"""
import argparse
import os
import struct
import sys
TEX_W = TEX_H = 128
TEX_BYTES = TEX_W * TEX_H * 4
MAX_TEXTURES = 16
# 00RRGGBB helper -----------------------------------------------------------
def px(r, g, b):
return bytes((b & 0xFF, g & 0xFF, r & 0xFF, 0))
# ---------------------------------------------------------------------------
# Procedural fallbacks (mirror include/engine_textures.inc)
# ---------------------------------------------------------------------------
def gen_brick():
out = bytearray()
for y in range(TEX_H):
for x in range(TEX_W):
r, g, b = 0x85, 0x22, 0x15
mortar = (y & 15) in (0, 1)
vx = x + (16 if (y & 16) else 0)
if (vx & 31) in (0, 1):
mortar = True
if mortar:
r = g = b = 0x55
else:
n = (((x ^ y) & 7) << 2)
b = min(255, b + n) # match ASM packed-color noise (low byte)
out += px(r, g, b)
return bytes(out)
def gen_wood():
out = bytearray()
for y in range(TEX_H):
for x in range(TEX_W):
r, g, b = 0x68, 0x3A, 0x12
if (x & 31) == 0:
r, g, b = 0x20, 0x10, 0x05
else:
n = ((y & 3) << 3)
b = min(255, b + n) # match ASM packed-color noise (low byte)
out += px(r, g, b)
return bytes(out)
def gen_stone():
out = bytearray()
for y in range(TEX_H):
for x in range(TEX_W):
r = g = b = 0x55
if (x & 31) == 0 or (y & 31) == 0:
r = g = b = 0x22
else:
n = ((((x * 3) + y) & 15) << 2)
b = min(255, b + n) # match ASM packed-color noise (low byte)
out += px(r, g, b)
return bytes(out)
def gen_metal():
out = bytearray()
for y in range(TEX_H):
for x in range(TEX_W):
r, g, b = 0x60, 0x65, 0x70
if (x - 10) ** 2 + (y - 10) ** 2 <= 16 or \
(x - 118) ** 2 + (y - 118) ** 2 <= 16:
r, g, b = 0xA0, 0xB0, 0xC0
out += px(r, g, b)
return bytes(out)
def gen_door():
out = bytearray()
for y in range(TEX_H):
for x in range(TEX_W):
if x < 8 or x >= 120 or y < 8:
r = g = b = 0x33
else:
r, g, b = 0x50, 0x2D, 0x15
if (x - 100) ** 2 + (y - 64) ** 2 <= 36:
r, g, b = 0xFF, 0xD0, 0x00
out += px(r, g, b)
return bytes(out)
def gen_pillar():
out = bytearray()
for y in range(TEX_H):
for x in range(TEX_W):
if (x & 15) < 8:
r, g, b = 0x70, 0xB0, 0xC0
else:
r, g, b = 0x40, 0x80, 0x90
out += px(r, g, b)
return bytes(out)
def gen_glass():
out = bytearray()
for y in range(TEX_H):
for x in range(TEX_W):
r, g, b = 0x40, 0x30, 0x20
if x < 6 or x >= 122 or y < 6 or y >= 122:
r = g = b = 0x60
else:
d = (x + y) & 63
if d < 4 or 24 <= d < 28:
r, g, b = 0x90, 0x70, 0x50
out += px(r, g, b)
return bytes(out)
def gen_marble():
out = bytearray()
for y in range(TEX_H):
for x in range(TEX_W):
r, g, b = 0xD8, 0xD0, 0xC0
v = (x * 7 + y * 13)
v = (v + (v >> 3)) & 63
if v <= 2 or v == 40:
r = g = b = 0x55
else:
n = (x ^ y) & 3
b = min(255, b + n) # match ASM packed-color noise (low byte)
out += px(r, g, b)
return bytes(out)
def gen_lamp():
"""Sprite texture slot 10: street lamp / torch with magenta chroma key."""
out = bytearray()
CK = (0xFF, 0x00, 0xFF)
for y in range(TEX_H):
for x in range(TEX_W):
r, g, b = CK
# pole
if 60 <= x < 68 and 40 <= y < 120:
r = g = b = 0x40
# lamp head
if 48 <= x < 80 and 16 <= y < 44:
r, g, b = 0xFF, 0xC0, 0x30
# base
if 52 <= x < 76 and 120 <= y < 126:
r = g = b = 0x30
out += px(r, g, b)
return bytes(out)
def gen_barrel():
"""Sprite texture slot 11: wooden barrel with chroma key."""
out = bytearray()
CK = (0xFF, 0x00, 0xFF)
cx = 64
for y in range(TEX_H):
for x in range(TEX_W):
r, g, b = CK
# barrel silhouette: ellipse-ish
dx = (x - cx) / 34.0
dy = (y - 70) / 52.0
if dx * dx + dy * dy <= 1.0:
r, g, b = 0x6A, 0x40, 0x1E
# metal bands
if abs(dy) > 0.55 and abs(dy) < 0.68:
r = g = b = 0x55
# highlight
if dx < -0.35 and abs(dy) < 0.5:
r, g, b = 0x8A, 0x5A, 0x2E
out += px(r, g, b)
return bytes(out)
def gen_statue():
"""Sprite texture slot 12: stone statue / column prop with chroma key."""
out = bytearray()
CK = (0xFF, 0x00, 0xFF)
for y in range(TEX_H):
for x in range(TEX_W):
r, g, b = CK
# pedestal
if 44 <= x < 84 and 112 <= y < 126:
r = g = b = 0x60
# body
if 54 <= x < 74 and 40 <= y < 112:
r = g = b = 0x8A + ((x ^ y) & 7)
# head
dx, dy = x - 64, y - 28
if dx * dx + dy * dy <= 100:
r = g = b = 0x9A
out += px(r, g, b)
return bytes(out)
GENERATORS = {
0: lambda: bytes(TEX_BYTES), # slot 0 = empty / solid marker
1: gen_brick,
2: gen_wood,
3: gen_stone,
4: gen_metal,
5: gen_door,
6: lambda: bytes(TEX_BYTES), # open door: never rendered
7: gen_pillar,
8: gen_glass,
9: gen_marble,
10: gen_lamp,
11: gen_barrel,
12: gen_statue,
13: lambda: px(255, 0, 255) * (TEX_W * TEX_H), # transparent reserved sprite
14: lambda: px(255, 0, 255) * (TEX_W * TEX_H), # transparent reserved sprite
15: lambda: px(255, 0, 255) * (TEX_W * TEX_H), # transparent reserved sprite
}
def main():
ap = argparse.ArgumentParser()
here = os.path.dirname(os.path.abspath(__file__))
ap.add_argument("--kazik", default=os.path.normpath(os.path.join(
here, "..", "..", "KAZIK", "assets", "textures", "casino3d-hd.bin")),
help="Path to KAZIK 48-slot texture pack (optional override)")
ap.add_argument("--out", default=os.path.normpath(os.path.join(
here, "..", "assets", "textures", "texturepack.bin")),
help="Output pack path")
args = ap.parse_args()
slots = [GENERATORS[i]() for i in range(MAX_TEXTURES)]
if os.path.isfile(args.kazik):
data = open(args.kazik, "rb").read()
avail = len(data) // TEX_BYTES
print(f"[pack] KAZIK pack found: {args.kazik} ({avail} slots)")
# KAZIK walls start at slot 1; map 1..min(9, avail) onto our 1..9
for slot in range(1, min(10, avail)):
src = data[slot * TEX_BYTES:(slot + 1) * TEX_BYTES]
if len(src) == TEX_BYTES:
slots[slot] = src
else:
print(f"[pack] KAZIK pack not found at {args.kazik} - full procedural pack")
os.makedirs(os.path.dirname(args.out), exist_ok=True)
with open(args.out, "wb") as f:
f.write(struct.pack("<IHH", 0x58544245, 1, MAX_TEXTURES)) # 'EBTX', v1
for s in slots:
assert len(s) == TEX_BYTES
f.write(s)
print(f"[pack] wrote {args.out} ({MAX_TEXTURES} slots, {8 + MAX_TEXTURES * TEX_BYTES} bytes)")
if __name__ == "__main__":
sys.exit(main())