"""Overhub Redliner — capture-safe overlay.

A transparent, click-through, always-on-top window laid over the Roblox client that draws the ESP (boxes, names, health,
tracers, off-screen arrows, glints, the aim ring, toasts) from the JSON the script writes ~30 times a second. The window is
flagged WDA_EXCLUDEFROMCAPTURE, so OBS (display / window capture), Discord screen share, Xbox Game Bar and Windows
screenshots do not see it, while your monitor does. OBS game capture hooks Roblox's own frames and never sees this window
either. Only a camera pointed at the screen or a capture card sees it.

Run:   python redliner_overlay.py            (finds the Potassium workspace, polls Overhub/redliner_overlay.json)
       python redliner_overlay.py --capturable   (debug: no capture exclusion, so a screenshot shows the drawing)
       python redliner_overlay.py --dir <folder holding Overhub/redliner_overlay.json>
"""
import ctypes, ctypes.wintypes as wt, json, os, sys, time, math, tkinter as tk, tkinter.font as tkfont

user32 = ctypes.windll.user32
try:
    ctypes.windll.shcore.SetProcessDpiAwareness(2)          # per-monitor DPI: client rects in physical pixels
except Exception:
    try: user32.SetProcessDPIAware()
    except Exception: pass

CAPTURABLE = "--capturable" in sys.argv
DIR = None
if "--dir" in sys.argv:
    DIR = sys.argv[sys.argv.index("--dir") + 1]
else:
    for cand in (os.path.join(os.environ.get("LOCALAPPDATA", ""), "Potassium", "workspace"),
                 os.path.join(os.environ.get("LOCALAPPDATA", ""), "Potassium", "Workspace"),
                 os.path.join(os.path.expanduser("~"), "Potassium", "workspace")):
        if os.path.isdir(cand): DIR = cand; break
if not DIR:
    print("workspace folder not found; pass --dir <folder>"); sys.exit(1)
DATA = os.path.join(DIR, "Overhub", "redliner_overlay.json")
ALIVE = os.path.join(DIR, "Overhub", "redliner_overlay.alive")
os.makedirs(os.path.dirname(DATA), exist_ok=True)

KEY = "#010101"            # the colour-key: everything painted in it is see-through
WDA_EXCLUDEFROMCAPTURE = 0x11
GWL_EXSTYLE = -20
WS_EX_LAYERED, WS_EX_TRANSPARENT, WS_EX_TOOLWINDOW, WS_EX_NOACTIVATE, WS_EX_TOPMOST = 0x80000, 0x20, 0x80, 0x8000000, 0x8

def find_roblox():
    hwnd = user32.FindWindowW("WINDOWSCLIENT", None)
    if not hwnd:
        # any window titled Roblox
        found = []
        def cb(h, _):
            n = ctypes.create_unicode_buffer(256); user32.GetWindowTextW(h, n, 256)
            if n.value == "Roblox" and user32.IsWindowVisible(h): found.append(h)
            return True
        user32.EnumWindows(ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_int, ctypes.c_int)(cb), 0)
        hwnd = found[0] if found else 0
    return hwnd

def client_rect(hwnd):
    r = wt.RECT(); user32.GetClientRect(hwnd, ctypes.byref(r))
    p = wt.POINT(0, 0); user32.ClientToScreen(hwnd, ctypes.byref(p))
    return p.x, p.y, r.right - r.left, r.bottom - r.top

root = tk.Tk()
root.title("ovh")
root.overrideredirect(True)
# not topmost: the window is OWNED by the Roblox window (set once Roblox is found), so Windows keeps it directly above
# Roblox and under anything you bring in front of Roblox, and hides it when Roblox minimizes
root.attributes("-transparentcolor", KEY)
root.configure(bg=KEY)
canvas = tk.Canvas(root, bg=KEY, highlightthickness=0, bd=0)
canvas.pack(fill="both", expand=True)
root.update_idletasks()
HWND = user32.GetParent(root.winfo_id()) or root.winfo_id()
ex = user32.GetWindowLongW(HWND, GWL_EXSTYLE)
user32.SetWindowLongW(HWND, GWL_EXSTYLE, ex | WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE)
GWLP_HWNDPARENT = -8
SWP_NOSIZE, SWP_NOMOVE, SWP_NOACTIVATE, SWP_FRAMECHANGED, SWP_SHOWWINDOW = 0x1, 0x2, 0x10, 0x20, 0x40
SetWindowLongPtrW = getattr(user32, "SetWindowLongPtrW", None) or user32.SetWindowLongW
SetWindowLongPtrW.restype = ctypes.c_void_p
SetWindowLongPtrW.argtypes = [wt.HWND, ctypes.c_int, ctypes.c_void_p]
def own_by(hwnd_owner):
    SetWindowLongPtrW(HWND, GWLP_HWNDPARENT, hwnd_owner)
    user32.SetWindowPos(HWND, 0, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW)
if not CAPTURABLE:
    ok = user32.SetWindowDisplayAffinity(HWND, WDA_EXCLUDEFROMCAPTURE)
    print("capture exclusion:", "on" if ok else "FAILED (needs Windows 10 2004+)", flush=True)
else:
    print("capture exclusion: OFF (--capturable)")

F = tkfont.Font(family="Consolas", size=10, weight="bold")
FS = tkfont.Font(family="Consolas", size=9)
FT = tkfont.Font(family="Consolas", size=11, weight="bold")

def rgb(c, default="#ffffff"):
    try:
        return "#%02x%02x%02x" % (int(c[0]), int(c[1]), int(c[2]))
    except Exception:
        return default

def dim(c, k):
    try:
        return "#%02x%02x%02x" % (int(c[0] * k), int(c[1] * k), int(c[2] * k))
    except Exception:
        return "#404040"

state = {"hwnd": 0, "rect": (0, 0, 0, 0), "lastRect": 0, "data": None, "mtime": 0, "toasts": [], "lastAlive": 0, "stale": 0}

def text_outlined(x, y, s, fill, font=F, anchor="s"):
    for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)):
        canvas.create_text(x + dx, y + dy, text=s, fill="#000000", font=font, anchor=anchor)
    canvas.create_text(x, y, text=s, fill=fill, font=font, anchor=anchor)

def draw(d, w, h):
    canvas.delete("all")
    if not d: return
    for e in d.get("esp") or []:
        cx, cy, bw, bh = e.get("x", 0), e.get("y", 0), e.get("w", 0), e.get("h", 0)
        col = rgb(e.get("c"))
        x0, y0, x1, y1 = cx - bw / 2, cy - bh / 2, cx + bw / 2, cy + bh / 2
        if e.get("arrow"):
            # off-screen: a triangle at the screen edge pointing at them
            ang = e.get("ang", 0)
            ax, ay = e.get("ax", cx), e.get("ay", cy)
            pts = []
            for a, r in ((0, 14), (2.5, 10), (-2.5, 10)):
                pts += [ax + math.cos(ang + a) * r, ay + math.sin(ang + a) * r]
            canvas.create_polygon(pts, fill=col, outline="#000000")
            continue
        style = e.get("box", "corners")
        if style == "full":
            canvas.create_rectangle(x0 - 1, y0 - 1, x1 + 1, y1 + 1, outline="#000000", width=3)
            canvas.create_rectangle(x0, y0, x1, y1, outline=col, width=1)
        elif style == "corners":
            arm = max(4, min(16, min(bw, bh) * 0.28))
            for (sx, sy, dx, dy) in ((x0, y0, 1, 1), (x1, y0, -1, 1), (x0, y1, 1, -1), (x1, y1, -1, -1)):
                canvas.create_line(sx, sy + arm * dy, sx, sy, sx + arm * dx, sy, fill="#000000", width=3)
                canvas.create_line(sx, sy + arm * dy, sx, sy, sx + arm * dx, sy, fill=col, width=1)
        if e.get("name"):
            text_outlined(cx, y0 - 3, e["name"], col, F, "s")
        if e.get("wep"):
            text_outlined(cx, y0 - 3 - 13, e["wep"], "#c8c8c8", FS, "s")
        if e.get("dist"):
            text_outlined(cx, y1 + 3, e["dist"], "#d0d0d0", FS, "n")
        hp = e.get("hp")
        if hp is not None:
            bx = x0 - 6
            canvas.create_rectangle(bx - 1, y0 - 1, bx + 3, y1 + 1, fill="#000000", outline="#000000")
            fy = y1 - (y1 - y0) * max(0.0, min(1.0, float(hp)))
            canvas.create_rectangle(bx, fy, bx + 2, y1, fill=rgb(e.get("hc"), "#2ee05a"), outline="")
        if e.get("read"):
            text_outlined(x1 + 5, cy, e["read"], rgb(e.get("rc"), "#ffffff"), FS, "w")
        tr = e.get("tr")
        if tr:
            canvas.create_line(tr[0], tr[1], cx, y1, fill=col, width=1)
        if e.get("look"):
            lk = e["look"]
            canvas.create_line(cx, y0, lk[0], lk[1], fill="#ff4040" if e.get("hot") else col, width=1)
    for g in d.get("glints") or []:
        col = rgb(g.get("c"))
        canvas.create_oval(g["x"] - 9, g["y"] - 9, g["x"] + 9, g["y"] + 9, outline=col, width=2)
        text_outlined(g["x"], g["y"] - 12, g.get("t", "[ DRAW ]"), col, FS, "s")
    ring = d.get("ring")
    if ring:
        canvas.create_oval(ring["x"] - ring["r"], ring["y"] - ring["r"], ring["x"] + ring["r"], ring["y"] + ring["r"], outline=rgb(ring.get("c"), "#ffffff"), width=1)
    lk = d.get("lock")
    if lk:
        canvas.create_line(lk[0], lk[1], lk[2], lk[3], fill="#ff4040", width=1)
    # the radar: rings, cardinal letters, the view cone, me, blips (dot or facing chevron), lock / glint rings, names
    rd = d.get("radar")
    if rd:
        cx, cy, r = rd["x"], rd["y"], rd["r"]
        yaw = rd.get("yaw", 0)
        heading = rd.get("heading", True)
        dial = -yaw if heading else 0
        ring_col = "#8a93b8"
        canvas.create_oval(cx - r - 1, cy - r - 1, cx + r + 1, cy + r + 1, outline="#000000", width=3)
        canvas.create_oval(cx - r, cy - r, cx + r, cy + r, outline="#ffd166" if rd.get("close") else ring_col, width=1)
        for k in (1 / 3, 2 / 3):
            rr = r * k
            canvas.create_oval(cx - rr, cy - rr, cx + rr, cy + rr, outline="#3a4160", width=1)
        canvas.create_line(cx - r, cy, cx + r, cy, fill="#2c3250", width=1)
        canvas.create_line(cx, cy - r, cx, cy + r, fill="#2c3250", width=1)
        for i, letter in enumerate(("N", "E", "S", "W")):
            a = math.radians(i * 90 + dial)
            text_outlined(cx + math.sin(a) * (r - 12), cy - math.cos(a) * (r - 12), letter, "#c8cde6", FS, "center")
        # the view cone, from me, hfov wide
        hf = math.radians(rd.get("hfov", 90) / 2)
        base = math.radians(-90 + (0 if heading else yaw))
        pts = [cx, cy]
        n = 10
        for i in range(n + 1):
            a = base - hf + (2 * hf) * i / n
            pts += [cx + math.cos(a) * r * 0.62, cy + math.sin(a) * r * 0.62]
        canvas.create_polygon(pts, fill="", outline="#5c6690", width=1)
        # me: a chevron pointing up (heading) or along the yaw (north-up)
        ma = math.radians(0 if heading else yaw)
        def rot(x, y, a):
            return (cx + x * math.cos(a) - y * math.sin(a), cy + x * math.sin(a) + y * math.cos(a))
        me = []
        for (x, y) in ((-4.5, 4.5), (0, -5.5), (4.5, 4.5), (0, 2)):
            me += rot(x, y, ma)
        canvas.create_polygon(me, fill="#ffffff", outline="#000000")
        for b in rd.get("blips") or []:
            bx, by = cx + b["x"], cy + b["y"]
            col = rgb(b.get("c"))
            if b.get("locked"):
                canvas.create_oval(bx - 8, by - 8, bx + 8, by + 8, outline="#ffd166", width=1)
            if b.get("drawing"):
                canvas.create_oval(bx - 11, by - 11, bx + 11, by + 11, outline=col, width=1)
            if b.get("dir") is not None:
                a = math.radians(b["dir"])
                pts = []
                for (x, y) in ((-4.5, 4.5), (0, -5.5), (4.5, 4.5), (0, 2)):
                    pts += [bx + x * math.cos(a) - y * math.sin(a), by + x * math.sin(a) + y * math.cos(a)]
                canvas.create_polygon(pts, fill=col, outline="#000000")
            else:
                rr = 2.4 if b.get("outside") else 3.5
                canvas.create_oval(bx - rr - 1, by - rr - 1, bx + rr + 1, by + rr + 1, fill="#000000", outline="")
                canvas.create_oval(bx - rr, by - rr, bx + rr, by + rr, fill=col, outline="")
            if b.get("up"):
                canvas.create_polygon(bx - 3, by - 6, bx, by - 10, bx + 3, by - 6, fill="#ffffff", outline="")
            if b.get("dn"):
                canvas.create_polygon(bx - 3, by + 6, bx, by + 10, bx + 3, by + 6, fill="#ffffff", outline="")
            if b.get("name"):
                text_outlined(bx, by + 6, b["name"], "#ffd166" if b.get("locked") else ("#ff5050" if b.get("drawing") else "#ffffff"), FS, "n")
        if rd.get("head"):
            text_outlined(cx, cy - r - 6, rd["head"], "#ffd166" if rd.get("close") else "#e0e4ff", F, "s")
        if rd.get("foot"):
            text_outlined(cx, cy + r + 6, rd["foot"], "#a0a6c8", FS, "n")
    # toasts (top right), newest at the top, 4 s each
    now = time.time()
    y = 48
    for t in [t for t in state["toasts"] if now - t["at"] < t.get("dur", 4)][:6]:
        s = (t.get("title") and (t["title"] + "  ·  ") or "") + str(t.get("text", ""))
        tw = FT.measure(s) + 24
        vpw = (d.get("vp") or [None])[0] or (w / (user32.GetDpiForWindow(state["hwnd"]) / 96.0 if state["hwnd"] else 1.0))   # viewport units
        canvas.create_rectangle(vpw - tw - 20, y, vpw - 20, y + 30, fill="#141622", outline="#4a4f6a")
        canvas.create_text(vpw - 32, y + 15, text=s, fill="#f0e6c8", font=FT, anchor="e")
        y += 36
    if d.get("demo"):
        text_outlined(w / 2, 24, "OVERHUB OVERLAY  ·  demo frame", "#ffd166", FT, "n")
    # the game's viewport is in logical (DPI-scaled) pixels; the window covers the physical client area: scale to fit
    vp = d.get("vp")
    if vp and vp[0] and vp[1]:
        sx, sy = w / float(vp[0]), h / float(vp[1])
    else:
        # older frames carry no viewport size: Roblox reports logical pixels, so the window's DPI scale is the ratio
        try:
            k = user32.GetDpiForWindow(state["hwnd"]) / 96.0 if state["hwnd"] else 1.0
        except Exception:
            k = 1.0
        sx, sy = k, k
    if abs(sx - 1) > 0.01 or abs(sy - 1) > 0.01:
        canvas.scale("all", 0, 0, sx, sy)

def tick():
    now = time.time()
    # follow the Roblox client area
    if now - state["lastRect"] > 0.1 or not state["hwnd"]:
        state["lastRect"] = now
        hwnd = state["hwnd"] if state["hwnd"] and user32.IsWindow(state["hwnd"]) else find_roblox()
        if hwnd and hwnd != state.get("owner"):
            own_by(hwnd); state["owner"] = hwnd
        state["hwnd"] = hwnd
        if hwnd:
            x, y, w, h = client_rect(hwnd)
            if (x, y, w, h) != state["rect"] and w > 50 and h > 50:
                state["rect"] = (x, y, w, h)
                root.geometry("%dx%d+%d+%d" % (w, h, x, y))
            if user32.IsIconic(hwnd): root.withdraw()
            else: root.deiconify()
    # heartbeat for the script (it falls back to in-game drawing when this goes stale)
    if now - state["lastAlive"] > 0.5:
        state["lastAlive"] = now
        try:
            with open(ALIVE, "w") as f: f.write(str(now))
        except Exception: pass
    # the data file
    try:
        m = os.path.getmtime(DATA)
        if m != state["mtime"]:
            state["mtime"] = m
            with open(DATA, "r", encoding="utf-8") as f:
                d = json.load(f)
            for t in d.get("toasts") or []:
                key = (t.get("id"), t.get("text"))
                if key not in [(x.get("id"), x.get("text")) for x in state["toasts"]]:
                    t["at"] = now; state["toasts"].insert(0, t)
            state["toasts"] = state["toasts"][:12]
            state["data"] = None if d.get("clear") else d
            if d.get("clear"): canvas.delete("all")
            state["stale"] = 0
        elif now - m > 1.0:
            state["stale"] += 1
            if state["stale"] == 30:
                state["data"] = None; canvas.delete("all")
                print("frames stopped — cleared", time.strftime("%H:%M:%S"), flush=True)
    except Exception:
        pass
    x, y, w, h = state["rect"]
    if state["data"] is not None and w > 0:
        draw(state["data"], w, h)
    root.after(16, tick)

print("overlay up — data:", DATA, flush=True)
root.after(16, tick)
root.mainloop()
