renpy-release-gate / index.html
getreadystack's picture
Upload index.html with huggingface_hub
daf3f97 verified
Raw History Blame Contribute Delete
45 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Renpy Release check — check your script free</title>
<meta name="description" content="This browser tool finds Ren&#x27;Py script issues including unescaped brackets and braces, define instead of default, tab indents, and shipped developer mode.">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/renpy-release-gate"></head>
<body>
<div class="wrap">
<h1>Renpy Release check — check your script free</h1>
<p class="lede">Catch the Ren'Py script bugs that compile fine and only crash in a player's save: unescaped [ and {, define-instead-of-default, tab indents, developer mode shipped. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false"># dirty.rpy - compiles, runs on your machine, breaks in a player's save.
define config.name = _("Harbour Lights")
define config.version = "1.0"
define config.developer = True
define config.console = True
define config.autoreload = True
define config.image_cache_size = 400
define affection = 0
init python:
build.name = "HarbourLights"
build.classify('game/**.txt', None)
image bg harbour = im.Scale("images/bg/harbour.png", 1920, 1080)
image logo = "C:/Users/dev/art/logo.png"
label start:
scene bg harbour with fade
play music "audio/theme.ogg" fadein 1.0
"Welcome back, [ player_name]."
"{i}Late again, she thought."
$ affection += 1
if persistent.seen_ending_a:
"You have stood on this pier before."
"The tab on this line is not indentation Ren'Py accepts."
return</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="paid"><strong>Free here:</strong> Scan one .rpy file and name every line that ships broken, with the fix for each.. <strong>In the editor:</strong> Gate the whole project at once - every .rpy file in the workspace - and export a release report you keep. — that part asks for a licence key.</div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var RENPY_RULES = [
{
"id": "unescaped_substitution",
"sev": "error",
"title": "Square bracket in dialogue is read as a variable",
"fix": "Escape it as \\[ , or close the interpolation with a real variable name."
},
{
"id": "unbalanced_text_tag",
"sev": "error",
"title": "Text tag opened and never closed",
"fix": "Close it, for example {i}...{/i}, or escape a literal brace as \\{."
},
{
"id": "define_not_default",
"sev": "error",
"title": "define used for a variable that changes during play",
"fix": "Use default instead of define so the value is saved and rolled back."
},
{
"id": "persistent_no_default",
"sev": "error",
"title": "persistent value read with no default",
"fix": "Add default persistent.NAME = ... so a first-time player does not read None."
},
{
"id": "tab_indent",
"sev": "error",
"title": "Tab used for indentation",
"fix": "Ren'Py indentation must be spaces. Replace the tab with spaces."
},
{
"id": "developer_mode_shipped",
"sev": "error",
"title": "config.developer is hard-set to True",
"fix": "Set config.developer = \"auto\" so the console and dev menu stay out of the build."
},
{
"id": "console_enabled",
"sev": "error",
"title": "config.console is left True",
"fix": "Remove the line or set it False before you build the distribution."
},
{
"id": "autoreload_enabled",
"sev": "warn",
"title": "config.autoreload is left True",
"fix": "Leave autoreload to developer mode; do not pin it True in a shipped build."
},
{
"id": "missing_save_directory",
"sev": "error",
"title": "config.save_directory is not set",
"fix": "Define config.save_directory once and never change it, so saves survive a rename."
},
{
"id": "placeholder_version",
"sev": "warn",
"title": "config.version is still the template placeholder",
"fix": "Set the real version; storefronts and crash reports read this string."
},
{
"id": "source_rpy_in_build",
"sev": "warn",
"title": "Build rules do not exclude .rpy source",
"fix": "Add build.classify('game/**.rpy', None) if you do not want readable script source in the distribution."
},
{
"id": "deprecated_im_api",
"sev": "warn",
"title": "Deprecated im.* image manipulator",
"fix": "Replace with a Transform or a Composite displayable."
},
{
"id": "removed_image_cache_size",
"sev": "error",
"title": "config.image_cache_size is the removed setting",
"fix": "Use config.image_cache_size_mb, which is measured in megabytes."
},
{
"id": "nonportable_asset_path",
"sev": "error",
"title": "Asset path will not resolve outside your machine",
"fix": "Use a relative, forward-slash path inside game/, e.g. \"images/bg/harbour.webp\"."
}
];</script>
<script>// Ren'Py Release Gate - one brain, used by the VS Code extension and by the free web page.
(function (root) {
'use strict';
var RULES = (typeof module !== 'undefined' && module.exports)
? require('./rules.json')
: root.RENPY_RULES;
var BY_ID = {};
RULES.forEach(function (r) { BY_ID[r.id] = r; });
var ASSET_EXT = /\.(png|jpg|jpeg|webp|webm|avi|ogg|opus|mp3|wav|ttf|otf|rpy|json|txt)$/i;
// Pull every string literal out of one line, keeping its 1-based column.
function strings(line) {
var out = [], i = 0, n = line.length;
while (i < n) {
var q = line[i];
if (q === '"' || q === "'") {
var j = i + 1, buf = '';
while (j < n) {
if (line[j] === '\\') { buf += line[j] + (line[j + 1] || ''); j += 2; continue; }
if (line[j] === q) break;
buf += line[j]; j++;
}
if (j >= n) break; // unterminated - leave it to Ren'Py
out.push(buf);
i = j + 1;
} else { i++; }
}
return out;
}
// Depth of an opener/closer pair, ignoring backslash-escaped ones.
function balance(s, open, close) {
var depth = 0, spaced = false;
for (var i = 0; i < s.length; i++) {
if (s[i] === '\\') { i++; continue; }
if (s[i] === open) {
depth++;
if (i + 1 >= s.length || s[i + 1] === ' ' || s[i + 1] === close) spaced = true;
} else if (s[i] === close) { depth--; }
}
return { depth: depth, spaced: spaced };
}
// Text tags that must be closed. {w} {p} {nw} {fast} and friends stand alone.
var PAIRED = ['a','alpha','alt','art','b','color','cps','font','i','k','noalt','outlinecolor','plain','rb','rt','s','size','u'];
// Name of the first text tag left open in this string, or '' if the string is sound.
function openTag(s) {
var stack = [], m, re = /\{([^{}]*)\}/g, clean = s.replace(/\\./g, '');
if ((clean.match(/\{/g) || []).length !== (clean.match(/\}/g) || []).length) return 'unclosed brace';
while ((m = re.exec(clean)) !== null) {
var body = m[1];
if (body.charAt(0) === '/') {
var want = body.slice(1);
if (stack.length && stack[stack.length - 1] === want) stack.pop();
else return '/' + want;
} else {
var name = body.split('=')[0].trim().toLowerCase();
if (PAIRED.indexOf(name) !== -1) stack.push(name);
}
}
return stack.length ? stack[stack.length - 1] : '';
}
function check(text, opts) {
opts = opts || {};
var src = String(text == null ? '' : text);
var lines = src.split(/\r?\n/);
var findings = [];
var seen = {};
function add(id, line, detail) {
var r = BY_ID[id];
findings.push({
check: id,
sev: r.sev,
msg: r.title + (detail ? ': ' + detail : '') + '. ' + r.fix,
line: line
});
}
var defines = {}; // bare name -> line of its define
var persistentDefault = {};
var persistentRead = {};
var buildLine = 0, hasBuild = false, excludesRpy = false;
var nameLine = 0, hasSaveDir = false;
var inInit = false;
lines.forEach(function (raw, idx) {
var ln = idx + 1;
var line = raw;
var code = line.replace(/#.*$/, '');
// init-block tracking: an init statement at column 0 opens a block.
if (/^\s*init\b[^:]*:\s*$/.test(code)) { inInit = true; }
else if (code.trim() && !/^\s/.test(code)) { inInit = false; }
if (/^[ ]*\t/.test(line)) add('tab_indent', ln);
if (/\bconfig\.developer\s*=\s*True\b/.test(code)) add('developer_mode_shipped', ln);
if (/\bconfig\.console\s*=\s*True\b/.test(code)) add('console_enabled', ln);
if (/\bconfig\.autoreload\s*=\s*True\b/.test(code)) add('autoreload_enabled', ln);
if (/\bconfig\.image_cache_size(?!_mb)\s*=/.test(code)) add('removed_image_cache_size', ln);
if (/\bconfig\.version\s*=\s*["']1\.0["']/.test(code)) add('placeholder_version', ln);
if (/\bconfig\.name\s*=/.test(code) && !nameLine) nameLine = ln;
if (/\bconfig\.save_directory\s*=/.test(code)) hasSaveDir = true;
if (/\bbuild\.[A-Za-z_]/.test(code)) { if (!hasBuild) { hasBuild = true; buildLine = ln; } }
if (/build\.classify\(\s*["'][^"']*\.rpy["']\s*,\s*None\s*\)/.test(code)) excludesRpy = true;
var im = code.match(/\bim\.(Scale|FactorScale|MatrixColor|Sepia|Grayscale|Composite|Crop|Flip|Tile|Alpha)\b/);
if (im) add('deprecated_im_api', ln, 'im.' + im[1]);
var def = code.match(/^\s*define\s+([A-Za-z_][A-Za-z0-9_]*)\s*=/);
if (def && !defines[def[1]]) defines[def[1]] = ln;
var pd = code.match(/^\s*default\s+persistent\.([A-Za-z_][A-Za-z0-9_]*)/);
if (pd) persistentDefault[pd[1]] = true;
var pw = code.match(/\bpersistent\.([A-Za-z_][A-Za-z0-9_]*)\s*=(?!=)/);
if (pw) { if (inInit) persistentDefault[pw[1]] = true; }
else {
var pr = code.match(/\bpersistent\.([A-Za-z_][A-Za-z0-9_]*)/);
if (pr && !/^\s*default\b/.test(code) && !persistentRead[pr[1]]) persistentRead[pr[1]] = ln;
}
strings(code).forEach(function (s) {
var sq = balance(s, '[', ']');
if (sq.depth !== 0 || sq.spaced) add('unescaped_substitution', ln, JSON.stringify(s.slice(0, 48)));
var tag = openTag(s);
if (tag) add('unbalanced_text_tag', ln, '{' + tag + '} in ' + JSON.stringify(s.slice(0, 40)));
if (ASSET_EXT.test(s) && (/\\/.test(s) || /^[A-Za-z]:/.test(s) || s.charAt(0) === '/' || s.indexOf('..') === 0)) {
add('nonportable_asset_path', ln, JSON.stringify(s.slice(0, 48)));
}
});
});
// define used for something that is reassigned later
Object.keys(defines).forEach(function (name) {
var re = new RegExp('^\\s*(?:\\$\\s*)?' + name + '\\s*(?:=(?!=)|\\+=|-=|\\*=|/=)');
for (var i = 0; i < lines.length; i++) {
if (i + 1 === defines[name]) continue;
if (re.test(lines[i].replace(/#.*$/, ''))) { add('define_not_default', defines[name], name); return; }
}
});
Object.keys(persistentRead).forEach(function (name) {
if (!persistentDefault[name]) add('persistent_no_default', persistentRead[name], 'persistent.' + name);
});
if (nameLine && !hasSaveDir) add('missing_save_directory', nameLine);
if (hasBuild && !excludesRpy) add('source_rpy_in_build', buildLine);
findings.sort(function (a, b) { return (a.line || 0) - (b.line || 0); });
findings = findings.filter(function (f) {
var k = f.check + '@' + f.line + '@' + f.msg;
if (seen[k]) return false; seen[k] = true; return true;
});
return { findings: findings, rule_count: RULES.length, today: opts.today || '' };
}
var api = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
if (typeof module !== 'undefined' && module.exports) module.exports = api;
root.RENPYENGINE = api;
})(typeof window !== 'undefined' ? window : globalThis);
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function summaryLine(res) {
const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
const f = (res && res.findings) || [];
const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); });
return lines.join('\n');
}
function toHtml(res) {
const f = (res && res.findings) || [];
if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var RENPYREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = RENPYREPORT;
else window.RENPYREPORT = RENPYREPORT;
</script>
<script>
(function(){
var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
today.value=new Date().toISOString().slice(0,10);
function run(){ try { var res=window.RENPYENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.RENPYREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
go.onclick=run; if (src.value.trim()) run();
})();
</script>
<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.renpy-release-gate" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'renpy-release-gate',src:'mp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/renpy-release-gate" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'renpy-release-gate',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/renpy-release-gate" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'renpy-release-gate',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/renpy-release-gate" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'renpy-release-gate',src:'mcp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/renpy-release-gate" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'renpy-release-gate',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.renpy-release-gate
npx @readystack/renpy-release-gate &lt;file&gt;
npx @readystack/renpy-release-gate --mcp</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 14 rules Ren'Py Release Gate checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"renpy-release-gate",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section>
<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Find the .rpy lines that compile clean and only break inside a player's save</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">Putting one title on Steam costs $100 in Steam Direct fees before a single crash report arrives, and every crash fix after that is another build, another store update and another refund.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_SbAs69NIkhPJABSgWl4Gx0cx54SRPDVWL5mHL3Tg52Q" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>
<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script>
<section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-14">Updated 2026-09-14</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-14","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku64314_result_card.jpg" alt="Ren&#x27;Py Release Gate" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>A 28-line Ren&#x27;Py script that compiles without one complaint returns 14 findings - 10 errors and 4 warnings - the moment a solo visual novel developer running a Steam page puts a release gate over it.</p>
<p>Not one of those 14 is a syntax error. Ren&#x27;Py already accepted the file. That is the trap: the Ren&#x27;Py compiler checks that your script parses, and almost nothing about whether a stranger can play it.</p>
<p>Here is the fixture, <code>_fixtures/dirty.rpy</code>, trimmed to the lines that matter:</p>
<pre><code>define config.developer = True
define config.version = &quot;1.0&quot;
define affection = 0
label start:
&quot;Welcome back, [ player_name].&quot;
&quot;{i}Late again, she thought.&quot;
$ affection += 1
if persistent.seen_ending_a:
&quot;You have stood on this pier before.&quot;</code></pre>
<p>Every one of those lines runs on your machine.</p>
<p><code>[ player_name]</code> is the clearest one. Ren&#x27;Py reads a square bracket in dialogue as an interpolation. The space makes the name unknown, and the error does not arrive at build time - it arrives when a player reaches that line of that scene. If the line lives in chapter three, you find out from a forum post.</p>
<p><code>{i}Late again, she thought.</code> opens an italic tag and never closes it. The scene keeps rendering, slanted, until something else closes it or the screen dies.</p>
<p><code>define affection = 0</code> is subtler and more expensive. In Ren&#x27;Py, <code>define</code> values are not written into saves and are not rolled back. A variable the player changes must be <code>default</code>. Ship it as <code>define</code> and your save files quietly disagree with your script, forever, for everyone who already bought the game.</p>
<p><code>persistent.seen_ending_a</code> is read with no <code>default persistent.seen_ending_a</code> anywhere. You will never see this bug. Your persistent file has held that key since the first test build. A first-time player reads <code>None</code> on the first run.</p>
<p>And <code>config.developer = True</code> hands every player the console and the developer menu.</p>
<p>Two more findings in that fixture never appear on a single line at all. There is no <code>config.save_directory</code> in the project, so saves are keyed to a generated directory and the next rename orphans them. And the build rules classify <code>game/<strong>.txt</code> but never <code>game/</strong>.rpy</code>, so readable script source travels inside the distribution.</p>
<p>Those last two are the reason a chatbot cannot close this. A chatbot sees the snippet you paste. Whether <code>persistent.seen_ending_a</code> is defaulted in some other file, and whether <code>config.save_directory</code> exists anywhere in the project, are facts about the whole project, and they are the two findings that cost you player saves.</p>
<p>The gate is 14 rules, grouped by what they take from you: four that break a player&#x27;s session, two that break their saves, four that ship something you did not mean to ship, four that break on a machine that is not yours.</p>
<p>The same 28-line fixture, gated, is <code>_fixtures/clean.rpy</code>: same scene, same dialogue, 0 findings. <code>\[07:40\]</code> escaped, <code>{i}...{/i}</code> closed, <code>default affection = 0</code>, <code>default persistent.seen_ending_a = False</code>, <code>config.developer = &quot;auto&quot;</code>, <code>config.save_directory</code> set once and never touched again, <code>build.classify(&#x27;game/**.rpy&#x27;, None)</code>.</p>
<p>The cost of getting this wrong is not abstract. Putting one title on Steam costs $100 in Steam Direct fees before a single crash report arrives, and every fix after launch is another build, another store update.</p>
<p>Open a <code>.rpy</code> file, run the check, read the line numbers. One file is free, with no key and no counter.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku64314_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku64314_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Ren&#x27;Py Release Gate actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads your .rpy files and reports the lines that pass Ren&#x27;Py&#x27;s compile step but fail later: a bare square bracket that Ren&#x27;Py reads as a variable, a text tag opened and never closed, define used where default belongs, a persistent value with no default, tab indentation, and shipped developer settings. Fourteen rules, each with the fix.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is it for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Solo and small-team Ren&#x27;Py developers shipping visual novels to Steam, itch.io and Android. It is aimed at the moment before you press build: the point where your own save file hides bugs that a first-time player will hit on line one of chapter three.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why is a chatbot not enough?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A chatbot only sees the snippet you paste. It cannot tell you that persistent.seen_ending_a is read in script.rpy and defaulted in none of your other files, or that config.save_directory is missing from the project. Those two findings need every file at once, and they are the two that lose player saves.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs a licence?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free: open a .rpy file, run the check, get every finding in that file with line numbers and fixes. That is a finished job for a single file, forever, with no key. The licence adds the other axis: one sweep across every .rpy file in the workspace and an exported release report you keep.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would this cost without the tool?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Putting one title on Steam costs $100 in Steam Direct fees before you see a crash report. After launch, each fix is a rebuild. The licence is $29 once, one key per person or CI seat,.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Ren&#x27;Py Release Gate checks the file you open against 14 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Ren'Py Release Gate actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads your .rpy files and reports the lines that pass Ren'Py's compile step but fail later: a bare square bracket that Ren'Py reads as a variable, a text tag opened and never closed, define used where default belongs, a persistent value with no default, tab indentation, and shipped developer settings. Fourteen rules, each with the fix."}}, {"@type": "Question", "name": "Who is it for?", "acceptedAnswer": {"@type": "Answer", "text": "Solo and small-team Ren'Py developers shipping visual novels to Steam, itch.io and Android. It is aimed at the moment before you press build: the point where your own save file hides bugs that a first-time player will hit on line one of chapter three."}}, {"@type": "Question", "name": "Why is a chatbot not enough?", "acceptedAnswer": {"@type": "Answer", "text": "A chatbot only sees the snippet you paste. It cannot tell you that persistent.seen_ending_a is read in script.rpy and defaulted in none of your other files, or that config.save_directory is missing from the project. Those two findings need every file at once, and they are the two that lose player saves."}}, {"@type": "Question", "name": "What is free and what needs a licence?", "acceptedAnswer": {"@type": "Answer", "text": "Free: open a .rpy file, run the check, get every finding in that file with line numbers and fixes. That is a finished job for a single file, forever, with no key. The licence adds the other axis: one sweep across every .rpy file in the workspace and an exported release report you keep."}}, {"@type": "Question", "name": "What would this cost without the tool?", "acceptedAnswer": {"@type": "Answer", "text": "Putting one title on Steam costs $100 in Steam Direct fees before you see a crash report. After launch, each fix is a rebuild. The licence is $29 once, one key per person or CI seat,."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Ren'Py Release Gate checks the file you open against 14 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:64314,slug:"renpy-release-gate",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/firmware-release-gate"><span class="yj-rel-t">Firmware Release Gate for sdkconfig and prj.conf</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/pyproject-release-gate"><span class="yj-rel-t">pyproject.toml Release Gate (PEP 639)</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/dotnet-eol-support-gate"><span class="yj-rel-t">.NET EOL &amp; CRA Support Window Gate</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/age-gate-lint"><span class="yj-rel-t">Age Gate Lint — UK Online Safety Act</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/cms-0057-api-gate"><span class="yj-rel-t">CMS-0057 API Gate</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/cargo-publish-gate"><span class="yj-rel-t">Cargo.toml Publish Gate - crates.io</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>