File size: 45,022 Bytes
f39affc
e6aad1b
 
 
 
daf3f97
 
e6aad1b
 
 
 
 
 
 
 
 
 
 
 
 
 
daf3f97
e6aad1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
daf3f97
 
 
 
e6aad1b
daf3f97
 
e6aad1b
 
 
 
 
daf3f97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f39affc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
<!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>