๐Ÿ›ก๏ธ Steam Game Protection Scanner โ€” Know the DRM Before You Download

:shield: Know What Youโ€™re Getting Into โ€” Before You Hit Install

Ever downloaded a game only to find out it wants three launchers, a blood sample, and a constant internet connection just to load the main menu? Yeah. That.


:world_map: What This Actually Does

A lightweight Python tool that scans any Steam game and tells you exactly whatโ€™s standing between you and actually playing it offline.

Plug in a Steam AppID โ†’ get a full protection breakdown in seconds.


:magnifying_glass_tilted_left: Why This Exists

Not Every Game Plays Nice โ€” Here's the Problem

You already know how to install games using Steam manifests from the previous guide:

:backhand_index_pointing_right: Steam Manifest Generators โ€” Full Toolkit: Download Any Game from Steamโ€™s Own Servers

But hereโ€™s the thing โ€” not every game cooperates. Some titles are wrapped in so many layers of DRM and third-party garbage that manifest installs either break or become pointless. Denuvo, always-online requirements, forced launcher redirects โ€” the list goes on.

So instead of downloading 80GB and then finding out it wonโ€™t launch, this tool tells you upfront what youโ€™re dealing with.

Think of it as a pre-flight check before you commit your bandwidth.


:high_voltage: What It Scans For

Full Detection Breakdown

The scanner pulls data from three sources simultaneously โ€” Steam Store API, SteamDB, and PCGamingWiki โ€” then cross-references everything to minimize false positives.

Hereโ€™s what gets flagged:

Protection Layer What It Means for You
:locked: Denuvo The big one. Heavily restricts offline and manifest-based installs
:video_game: Steam DRM Standard Steamworks DRM โ€” usually manageable, but still noted
:rocket: Third-Party Launchers Rockstar, Ubisoft Connect, EA App, Battle.net, 2K, Bethesda โ€” the whole circus
:bust_in_silhouette: Account Required Needs a separate login beyond Steam โ€” extra friction
:globe_with_meridians: Always Online No internet = no game. Deal-breaker for offline setups
:shield: Anti-Cheat Easy Anti-Cheat, BattlEye, PunkBuster โ€” relevant for modding and compatibility

Every detection includes evidence keywords so you can see exactly what triggered each flag. No guessing.


:bar_chart: The Offline Readiness Score

0โ€“100 Scale โ€” Instant Verdict

After scanning, the tool spits out a score from 0 to 100:

Score Verdict What It Means
80โ€“100 :white_check_mark: Excellent Clean install, minimal restrictions โ€” youโ€™re good
50โ€“79 :warning: Limited Some hurdles โ€” might need workarounds
0โ€“49 :cross_mark: Poor Stacked protections โ€” expect problems with offline/manifest installs

How scoring works:

  • Start at 100 (clean slate)
  • Denuvo detected โ†’ โˆ’40
  • Always online โ†’ โˆ’40
  • Third-party launcher โ†’ โˆ’25
  • Account required โ†’ โˆ’20
  • Steam DRM โ†’ โˆ’10

Penalties stack. A game with Denuvo + always-online + a forced launcher is already sitting at 0 before you even look at the rest.


:desktop_computer: What It Looks Like

The GUI is minimal โ€” intentionally. One input field, one button, full results.

Punch in any Steam AppID (that number in the store URL), hit Analyze, and the full report drops in seconds.


:brain: How It Works Under the Hood

Smart Detection โ€” Not Just Keyword Spam

This isnโ€™t a dumb string matcher. The scanner uses negation-aware context checking โ€” meaning if a page says โ€œdoes not use Denuvoโ€ or โ€œno longer requires always online,โ€ it wonโ€™t false-flag it.

A 30-character window before each keyword match gets checked for negation patterns like โ€œno,โ€ โ€œnot,โ€ โ€œwithout,โ€ โ€œdoes not,โ€ โ€œdoesnโ€™t,โ€ and โ€œno longer.โ€

Data pipeline:

  1. Steam Store API โ†’ pulls DRM notices, system requirements, and descriptions
  2. SteamDB โ†’ deeper metadata and historical protection info (auto-falls back to proxy if blocked)
  3. PCGamingWiki โ†’ community-maintained protection and compatibility data

All three sources get analyzed independently, then merged. If any source flags a protection, it shows up โ€” along with which keywords triggered it.


:clipboard: Requirements & Setup

Getting It Running

You need Python with two packages:

pip install requests PyQt6

Save the script, run it, enter an AppID. Thatโ€™s the whole setup.

Dependencies:

  • requests โ€” handles all the HTTP calls to Steam, SteamDB, and PCGamingWiki
  • PyQt6 โ€” the GUI framework

No API keys. No accounts. No config files. Just run it.


:wrench: The Full Script

Complete Python Source โ€” Copy and Go
import re
import sys
from dataclasses import dataclass
from typing import Dict, Iterable, List, Optional

import requests
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
    QApplication,
    QLabel,
    QLineEdit,
    QPushButton,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)

HEADERS = {"User-Agent": "Mozilla/5.0"}
REQUEST_TIMEOUT = 10

# ---------- Signatures ----------

LAUNCHERS = {
    "rockstar": "Rockstar Games Launcher",
    "social club": "Rockstar Games Launcher",
    "ubisoft": "Ubisoft Connect",
    "uplay": "Ubisoft Connect",
    "ea app": "EA App",
    "origin": "EA App",
    "battle.net": "Battle.net",
    "2k launcher": "2K Launcher",
    "bethesda.net": "Bethesda Launcher",
}

ACCOUNT_SIGNS = [
    "requires third-party account",
    "requires account",
    "online activation",
    "social club account",
    "ea account",
    "ubisoft account",
]

ANTICHEAT = {
    "easy anti-cheat": "Easy Anti-Cheat",
    "battleye": "BattlEye",
    "punkbuster": "PunkBuster",
}

STEAM_DRM_SIGNS = [
    "steam drm",
    "steamworks drm",
    "steamworks (drm)",
]

NEGATION_PATTERNS = [
    "no ",
    "not ",
    "without ",
    "does not ",
    "doesn't ",
    "no longer ",
]


def _fetch_text(session: requests.Session, url: str) -> str:
    response = session.get(url, headers=HEADERS, timeout=REQUEST_TIMEOUT)
    response.raise_for_status()
    return response.text.lower()


@dataclass
class SteamStoreEntry:
    name: str
    text: str


def steam_store_data(session: requests.Session, app_id: str) -> Optional[SteamStoreEntry]:
    url = "https://store.steampowered.com/api/appdetails"
    params = {"appids": app_id, "cc": "us", "l": "en"}
    response = session.get(url, params=params, timeout=REQUEST_TIMEOUT)
    response.raise_for_status()
    data = response.json()

    if not data.get(str(app_id), {}).get("success"):
        return None

    details = data[str(app_id)]["data"]
    name = details.get("name", "Unknown title")
    store_text = (
        details.get("drm_notice", "")
        + str(details.get("pc_requirements", ""))
        + details.get("short_description", "")
    ).lower()
    return SteamStoreEntry(name=name, text=store_text)


def steamdb_text(session: requests.Session, app_id: str) -> str:
    url = f"https://steamdb.info/app/{app_id}/"
    try:
        return _fetch_text(session, url)
    except requests.HTTPError as exc:
        if exc.response is not None and exc.response.status_code == 403:
            proxy_url = f"https://r.jina.ai/https://steamdb.info/app/{app_id}/"
            return _fetch_text(session, proxy_url)
        raise


def pcgamingwiki_text(session: requests.Session, game_name: str) -> str:
    name = game_name.replace(" ", "_")
    url = f"https://www.pcgamingwiki.com/wiki/{name}"
    return _fetch_text(session, url)


def _has_term(text: str, term: str) -> bool:
    for match in re.finditer(re.escape(term), text):
        start = max(match.start() - 30, 0)
        context = text[start:match.start()]
        if any(neg in context for neg in NEGATION_PATTERNS):
            continue
        return True
    return False


def _gather_terms(text: str, terms: Iterable[str]) -> List[str]:
    hits = []
    for term in terms:
        if _has_term(text, term):
            hits.append(term)
    return hits


def analyze_text(text: str) -> Dict[str, object]:
    result = {
        "denuvo": False,
        "steam_drm": False,
        "launchers": set(),
        "account_required": False,
        "always_online": False,
        "anticheats": set(),
        "evidence": {
            "denuvo": [],
            "steam_drm": [],
            "launchers": [],
            "account_required": [],
            "always_online": [],
            "anticheats": [],
        },
    }

    if _has_term(text, "denuvo"):
        result["denuvo"] = True
        result["evidence"]["denuvo"].append("denuvo")

    steam_drm_hits = _gather_terms(text, STEAM_DRM_SIGNS)
    if steam_drm_hits:
        result["steam_drm"] = True
        result["evidence"]["steam_drm"].extend(steam_drm_hits)

    for key, name in LAUNCHERS.items():
        if _has_term(text, key):
            result["launchers"].add(name)
            result["evidence"]["launchers"].append(name)

    account_hits = _gather_terms(text, ACCOUNT_SIGNS)
    if account_hits:
        result["account_required"] = True
        result["evidence"]["account_required"].extend(account_hits)

    if _has_term(text, "always online"):
        result["always_online"] = True
        result["evidence"]["always_online"].append("always online")

    for key, name in ANTICHEAT.items():
        if _has_term(text, key):
            result["anticheats"].add(name)
            result["evidence"]["anticheats"].append(name)

    return result


def _merge_sets(target: Dict[str, object], source: Dict[str, object], key: str) -> None:
    if source.get(key):
        target[key].update(source[key])


def _merge_evidence(target: Dict[str, object], source: Dict[str, object]) -> None:
    for key, values in source.get("evidence", {}).items():
        if values:
            target["evidence"].setdefault(key, []).extend(values)


def merge_results(*results: Optional[Dict[str, object]]) -> Dict[str, object]:
    final = {
        "denuvo": False,
        "steam_drm": False,
        "launchers": set(),
        "account_required": False,
        "always_online": False,
        "anticheats": set(),
        "evidence": {
            "denuvo": [],
            "steam_drm": [],
            "launchers": [],
            "account_required": [],
            "always_online": [],
            "anticheats": [],
        },
    }

    for r in results:
        if not r:
            continue
        if r.get("denuvo"):
            final["denuvo"] = True
        if r.get("steam_drm"):
            final["steam_drm"] = True
        if r.get("account_required"):
            final["account_required"] = True
        if r.get("always_online"):
            final["always_online"] = True
        _merge_sets(final, r, "launchers")
        _merge_sets(final, r, "anticheats")
        _merge_evidence(final, r)

    return final


def offline_score(result: Dict[str, object]) -> int:
    score = 100
    if result["denuvo"]:
        score -= 40
    if result["launchers"]:
        score -= 25
    if result["account_required"]:
        score -= 20
    if result["always_online"]:
        score -= 40
    if result["steam_drm"]:
        score -= 10
    return max(score, 0)


def _format_list(items: Iterable[str]) -> str:
    values = sorted(items)
    return ", ".join(values) if values else "NO"


def _format_evidence(items: Iterable[str]) -> str:
    values = sorted(set(items))
    return ", ".join(values) if values else "โ€”"


class ProtectionChecker(QWidget):
    def __init__(self) -> None:
        super().__init__()
        self.setWindowTitle("Steam Protection Analyzer")
        self.setGeometry(300, 300, 520, 520)

        layout = QVBoxLayout()

        layout.addWidget(QLabel("Steam AppID:"))

        self.input = QLineEdit()
        self.input.setPlaceholderText("Example: 1174180 (Red Dead Redemption 2)")
        layout.addWidget(self.input)

        self.button = QPushButton("Analyze")
        self.button.clicked.connect(self.run_check)
        layout.addWidget(self.button)

        self.game_name_label = QLabel("Game: โ€”")
        self.game_name_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
        layout.addWidget(self.game_name_label)

        self.output = QTextEdit()
        self.output.setReadOnly(True)
        layout.addWidget(self.output)

        self.setLayout(layout)

        self.session = requests.Session()

    def run_check(self) -> None:
        app_id = self.input.text().strip()
        if not app_id.isdigit():
            self.output.setText("Invalid AppID.")
            self.game_name_label.setText("Game: โ€”")
            return

        self.button.setEnabled(False)
        self.output.setText("Analyzing...\n")
        self.game_name_label.setText("Game: โ€”")
        QApplication.processEvents()

        try:
            store = steam_store_data(self.session, app_id)
        except requests.RequestException as exc:
            self.output.setText(f"Steam Store request failed: {exc}")
            self.button.setEnabled(True)
            return

        if not store:
            self.output.setText("Game not found.")
            self.button.setEnabled(True)
            return

        name = store.name
        self.game_name_label.setText(f"Game: {name}")

        errors: List[str] = []
        steamdb = ""
        wiki = ""
        try:
            steamdb = steamdb_text(self.session, app_id)
        except requests.RequestException as exc:
            errors.append(f"SteamDB unavailable ({exc})")

        try:
            wiki = pcgamingwiki_text(self.session, name)
        except requests.RequestException as exc:
            errors.append(f"PCGamingWiki unavailable ({exc})")

        r1 = analyze_text(store.text)
        r2 = analyze_text(steamdb) if steamdb else None
        r3 = analyze_text(wiki) if wiki else None

        final = merge_results(r1, r2, r3)
        score = offline_score(final)

        result = (
            f"Game: {name}\n\n"
            "Protection summary:\n"
            f"- Denuvo: {'YES' if final['denuvo'] else 'NO'}\n"
            f"- Steam DRM: {'YES' if final['steam_drm'] else 'NO'}\n"
            f"- Third-party launcher: {_format_list(final['launchers'])}\n"
            f"- Account required: {'YES' if final['account_required'] else 'NO'}\n"
            f"- Always online: {'YES' if final['always_online'] else 'NO'}\n"
            f"- Anti-cheat: {_format_list(final['anticheats'])}\n\n"
            f"Offline readiness score: {score}/100\n"
        )

        if score >= 80:
            result += "\nโœ… Excellent for offline play"
        elif score >= 50:
            result += "\nโš  Limited offline usability"
        else:
            result += "\nโŒ Poor offline experience"

        if errors:
            result += "\n\nWarnings:\n- " + "\n- ".join(errors)

        result += (
            "\n\nEvidence (keywords found):\n"
            f"- Denuvo: {_format_evidence(final['evidence']['denuvo'])}\n"
            f"- Steam DRM: {_format_evidence(final['evidence']['steam_drm'])}\n"
            f"- Launchers: {_format_evidence(final['evidence']['launchers'])}\n"
            f"- Account required: {_format_evidence(final['evidence']['account_required'])}\n"
            f"- Always online: {_format_evidence(final['evidence']['always_online'])}\n"
            f"- Anti-cheat: {_format_evidence(final['evidence']['anticheats'])}\n"
        )

        self.output.setText(result)
        self.button.setEnabled(True)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = ProtectionChecker()
    window.show()
    sys.exit(app.exec())

:light_bulb: Quick Usage Example

Step-by-Step โ€” 30 Seconds Flat
  1. Open the app
  2. Go to any Steam store page โ€” grab the number from the URL (e.g., 1174180 for Red Dead Redemption 2)
  3. Paste it in the AppID field
  4. Hit Analyze
  5. Read your report โ€” Denuvo? Launcher hell? Always-online nonsense? Itโ€™s all there

Thatโ€™s it. Now you know whether to bother with the manifest install or save yourself the headache.


:bullseye: Who This Is For

  • You use Steam manifest generators and want to check compatibility before downloading
  • You grab games from other sources and want to know what DRM landmines are waiting
  • You play offline a lot and need to know which games will actually cooperate
  • Youโ€™re just curious about what protections your library is running

One AppID. Full x-ray. No surprises. :shield:

7 Likes

its work thank

1 Like

Thereโ€™s a lot of interesting things ahead; the gaming theme will be actively developing.

1 Like