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.
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.
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:
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.
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 |
|---|---|
| The big one. Heavily restricts offline and manifest-based installs | |
| Standard Steamworks DRM โ usually manageable, but still noted | |
| Rockstar, Ubisoft Connect, EA App, Battle.net, 2K, Bethesda โ the whole circus | |
| Needs a separate login beyond Steam โ extra friction | |
| No internet = no game. Deal-breaker for offline setups | |
| 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.
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 | Clean install, minimal restrictions โ youโre good | |
| 50โ79 | Some hurdles โ might need workarounds | |
| 0โ49 | 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.
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.
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:
- Steam Store API โ pulls DRM notices, system requirements, and descriptions
- SteamDB โ deeper metadata and historical protection info (auto-falls back to proxy if blocked)
- 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.
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 PCGamingWikiPyQt6โ the GUI framework
No API keys. No accounts. No config files. Just run it.
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())
Quick Usage Example
Step-by-Step โ 30 Seconds Flat
- Open the app
- Go to any Steam store page โ grab the number from the URL (e.g.,
1174180for Red Dead Redemption 2) - Paste it in the AppID field
- Hit Analyze
- 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.
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. ![]()


!