- 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
255 lines
8.3 KiB
Python
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())
|