#!/usr/bin/env python3 # Mudancas no projeto Ball2Box (dulvui/ball2box, commit cb98f96, pasta game/) feitas por build-ball2box.sh. O # jogo tem botoes que abrem sites externos (OS.shell_open) para o site e o GitHub do autor, doacao, lojas de # aplicativos e Mastodon; na web isso viraria links para fora do nosso site, entao saem: # 1. Tela principal: botao do GitHub e o link "simondalvai.org". # 2. Tela de informacoes: botoes Website, GitHub e Donate; os links do texto de creditos deixam de abrir paginas # (o texto, com as licencas, fica). # 3. Loja de bolas: as duas bolas que se desbloqueavam abrindo o Mastodon do autor ("FOLLOW") e a pagina de # outros jogos ("MORE_GAMES") passam a custar 0 estrelas, sem abrir nada. # 4. Tela de ajuda: o botao escondido "how to video" (um link para o YouTube) sai. # 5. Aviso de modificacao (AGPL-3.0 secao 5a) na tela de informacoes e no fonte publicado. # 6. assets/i18n/UltimateTossi18n.csv.import passa a importer="keep": o Godot reimportaria o CSV gerando as # traducoes com uma ordem que muda a cada execucao (chaves ordenadas por endereco de memoria), e o export nao # seria reprodutivel; assim ele usa os .translation que o autor ja versionou no repositorio (mesmo texto). # 7. O botao de informacoes da tela principal, que o autor deixou escondido nesta versao, passa a aparecer: a tela # dele traz os creditos e licencas, e o aviso de modificacao do item 5. # Uso: python3 -I patch.py import re, sys, os root = sys.argv[1] def p(*a): return os.path.join(root, *a) def read(f): return open(p(f), encoding='utf-8').read() def write(f, s): open(p(f), 'w', encoding='utf-8').write(s) def drop_nodes(text, paths): """Remove nos do .tscn (e descendentes) e as conexoes que partem deles.""" blocks = re.split(r'(?m)^(?=\[)', text) out = [] for b in blocks: m = re.match(r'\[node name="([^"]+)"(?: type="[^"]+")?(?: parent="([^"]*)")?', b) if m: name, parent = m.group(1), m.group(2) full = name if parent is None else (name if parent == '.' else parent + '/' + name) if parent is not None and any(full == x or full.startswith(x + '/') for x in paths): continue m = re.match(r'\[connection signal="[^"]+" from="([^"]+)"', b) if m and any(m.group(1) == x or m.group(1).startswith(x + '/') for x in paths): continue out.append(b) return ''.join(out) def drop_funcs(code, names): for fn in names: code, k = re.subn(r'(?ms)^func %s\(.*?(?=^func |\Z)' % fn, '', code) assert k == 1, fn return code def count_nodes(t): return t.count('[node ') # 1. Tela principal. t = read('src/screens/main/Main.tscn'); n0 = count_nodes(t) t = drop_nodes(t, ['Buttons/Settings/GithubButton', 'Buttons/SimonDalvai']) assert n0 - count_nodes(t) == 2 t, k = re.subn(r'(?m)^\[ext_resource path="res://assets/github\.svg"[^\n]*\n', '', t); assert k == 1 write('src/screens/main/Main.tscn', t) g = drop_funcs(read('src/screens/main/Main.gd'), ['_on_SimonDalvai_pressed', '_on_GithubButton_pressed']) write('src/screens/main/Main.gd', g) # 2. Tela de informacoes. t = read('src/screens/info/Info.tscn'); n0 = count_nodes(t) t = drop_nodes(t, ['VBoxContainer/Buttons/Website', 'VBoxContainer/Buttons/Github', 'VBoxContainer/Buttons/Donate']) assert n0 - count_nodes(t) == 3 write('src/screens/info/Info.tscn', t) g = read('src/screens/info/Info.gd') g = drop_funcs(g, ['_ready', '_on_Website_pressed', '_on_Github_pressed', '_on_Donate_pressed']) g, k = re.subn(r'\tOS\.shell_open\(meta\)\n', '\tpass # fredygames: os links do texto nao abrem paginas externas\n', g); assert k == 1 write('src/screens/info/Info.gd', g.rstrip('\n') + '\n') # 3. Loja de bolas. g = read('src/screens/shop/Shop.gd') g, k = re.subn(r'(?ms)\t\t# special cases like follow, more games etc\.\.\.\n.*?(?=\n\tif result > 0:)', '', g); assert k == 1 write('src/screens/shop/Shop.gd', g) b = read('src/utils/BallMachine.gd') assert b.count('"price": tr("MORE_GAMES")') == 1 and b.count('"price": tr("FOLLOW")') == 1 b = b.replace('"price": tr("MORE_GAMES")', '"price": 0').replace('"price": tr("FOLLOW")', '"price": 0') write('src/utils/BallMachine.gd', b) # 4. Tela de ajuda. t = read('src/screens/help/Help.tscn'); n0 = count_nodes(t) t = drop_nodes(t, ['VBoxContainer/Video']) assert n0 - count_nodes(t) == 1 write('src/screens/help/Help.tscn', t) write('src/screens/help/Help.gd', drop_funcs(read('src/screens/help/Help.gd'), ['_on_Video_pressed']).rstrip('\n') + '\n') # Nenhuma chamada a OS.shell_open pode ter sobrado. for dp, dn, fn in os.walk(root): for f in fn: if f.endswith('.gd'): assert 'shell_open' not in open(os.path.join(dp, f), encoding='utf-8').read(), f # 5. Aviso de modificacao (AGPL-3.0 secao 5a) na tela de informacoes e no fonte publicado. t = read('src/screens/info/Info.tscn') old = 'This game is open source under the' assert t.count(old) == 2 note = ('Modified for FredyGames on 2026-10-29: links to external sites removed. ' 'Source code of this version: see the game page on fredygames.com.\\n\\n') t = t.replace(old, note + old) write('src/screens/info/Info.tscn', t) # 6. Traducoes: usar os .translation versionados, sem reimportar o CSV (export reprodutivel). c = read('assets/i18n/UltimateTossi18n.csv.import') assert 'importer="csv_translation"' in c for f in ('en', 'de', 'it', 'es'): assert os.path.exists(p('assets/i18n/UltimateTossi18n.%s.translation' % f)) write('assets/i18n/UltimateTossi18n.csv.import', '[remap]\n\nimporter="keep"\n') # 7. Botao de informacoes visivel (creditos, licencas e o aviso de modificacao). t = read('src/screens/main/Main.tscn') t, k = re.subn(r'(\[node name="InfoButton" type="Button" parent="Buttons/Settings"\]\n)visible = false\n', r'\1', t) assert k == 1 write('src/screens/main/Main.tscn', t)