gamemaker / app.py
Cnass's picture
Update app.py
a2c6f6c verified
Raw History Blame Contribute Delete
13.3 kB
import os
import zipfile
import tempfile
import json
import subprocess
import time
import uuid
import glob
import struct
from fastapi import FastAPI, UploadFile, File, BackgroundTasks
from fastapi.responses import FileResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
# ==========================================
# C# SCRIPT FABRİKASI (KUSURSUZ X-RAY GÖRSEL ÇIKARICI)
# ==========================================
def generate_csx_scripts():
extract_code = """using System.IO;
using System.Text.Json;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UndertaleModLib;
// YENİLENMİŞ X-RAY: Hem obje içindeki hem de doğrudan çıplak (byte[]) olan resmi bulur.
byte[] GetImg(UndertaleEmbeddedTexture emb) {
if (emb == null || emb.TextureData == null) return null;
object td = emb.TextureData;
if (td is byte[] b) return b; // Eğer resim doğrudan çıplak haldeyse direkt al!
var t = td.GetType();
var p = t.GetProperties().FirstOrDefault(x => x.PropertyType == typeof(byte[]));
if (p != null) return p.GetValue(td) as byte[];
var f = t.GetFields().FirstOrDefault(x => x.FieldType == typeof(byte[]));
if (f != null) return f.GetValue(td) as byte[];
return null;
}
var dict = new Dictionary<string, string>();
foreach (var str in Data.Strings) {
if (!string.IsNullOrWhiteSpace(str.Content)) {
if(!dict.ContainsKey(str.Content)) dict.Add(str.Content, str.Content);
}
}
var opt = new JsonSerializerOptions { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
File.WriteAllText("strings.json", JsonSerializer.Serialize(dict, opt));
// FONT VE RESİMLERİ (ATLAS) ÇIKAR
Directory.CreateDirectory("Fonts");
var fontData = new Dictionary<string, object>();
foreach (var font in Data.Fonts) {
if (font.Texture != null && font.Texture.TexturePage != null) {
var emb = font.Texture.TexturePage;
// İsmi yoksa bile özel isim uydurarak o PNG'yi zorla kaydeder
string texName = emb.Name?.Content;
if (string.IsNullOrEmpty(texName)) texName = "Texture_" + font.Name.Content;
string texPath = "Fonts/" + texName + ".png";
if (!File.Exists(texPath)) {
byte[] img = GetImg(emb);
if (img != null) {
File.WriteAllBytes(texPath, img);
}
}
var glyphs = new Dictionary<string, object>();
foreach (var g in font.Glyphs) {
glyphs[g.Character.ToString()] = new { x = g.SourceX, y = g.SourceY, w = g.SourceWidth, h = g.SourceHeight, shift = g.Shift, offset = g.Offset };
}
fontData[font.Name.Content] = new { Texture = texName, Glyphs = glyphs };
}
}
File.WriteAllText("Fonts/font_data.json", JsonSerializer.Serialize(fontData, opt));
"""
repack_code = """using System;
using System.IO;
using System.Text.Json;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UndertaleModLib;
// YENİLENMİŞ X-RAY: Değiştirilen PNG resmini oyun dosyasına kusursuzca gömer
void SetImg(UndertaleEmbeddedTexture emb, byte[] d) {
if (emb == null || emb.TextureData == null) return;
object td = emb.TextureData;
if (td is byte[]) {
// Bazı UMT sürümlerinde TextureData sadece bir byte dizisidir
var prop = emb.GetType().GetProperty("TextureData");
if (prop != null && prop.CanWrite) prop.SetValue(emb, d);
return;
}
var t = td.GetType();
var p = t.GetProperties().FirstOrDefault(x => x.PropertyType == typeof(byte[]) && x.CanWrite);
if (p != null) { p.SetValue(td, d); return; }
var f = t.GetFields().FirstOrDefault(x => x.FieldType == typeof(byte[]));
if (f != null) { f.SetValue(td, d); return; }
}
string json = File.ReadAllText("strings.json");
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
foreach (var str in Data.Strings) {
if (dict.TryGetValue(str.Content, out string newStr)) {
str.Content = newStr;
}
}
// FONT RESİMLERİNİ VE KOORDİNAT HARİTASINI GERİ YÜKLE
if (Directory.Exists("Fonts")) {
string fontJsonPath = "Fonts/font_data.json";
if (File.Exists(fontJsonPath)) {
string fJson = File.ReadAllText(fontJsonPath);
var fDict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(fJson);
foreach (var font in Data.Fonts) {
if (fDict.TryGetValue(font.Name.Content, out JsonElement fData)) {
// Görseli Güncelle
if (fData.TryGetProperty("Texture", out JsonElement texElement)) {
string texName = texElement.GetString();
string texPath = "Fonts/" + texName + ".png";
if (File.Exists(texPath) && font.Texture != null && font.Texture.TexturePage != null) {
SetImg(font.Texture.TexturePage, File.ReadAllBytes(texPath));
}
}
// Koordinatları Güncelle
if (fData.TryGetProperty("Glyphs", out JsonElement glyphsJson)) {
foreach (var glyphProp in glyphsJson.EnumerateObject()) {
ushort charCode;
if (!ushort.TryParse(glyphProp.Name, out charCode)) continue;
var gData = glyphProp.Value;
var existingGlyph = font.Glyphs.FirstOrDefault(g => g.Character == charCode);
if (existingGlyph == null) {
existingGlyph = new UndertaleFont.Glyph { Character = charCode };
font.Glyphs.Add(existingGlyph);
}
existingGlyph.SourceX = gData.GetProperty("x").GetUInt16();
existingGlyph.SourceY = gData.GetProperty("y").GetUInt16();
existingGlyph.SourceWidth = gData.GetProperty("w").GetUInt16();
existingGlyph.SourceHeight = gData.GetProperty("h").GetUInt16();
existingGlyph.Shift = gData.GetProperty("shift").GetInt16();
existingGlyph.Offset = gData.GetProperty("offset").GetInt16();
}
}
}
}
}
}
// Reflection ile Dinamik Kaydetme
using (var stream = new FileStream("repacked_game.droid", FileMode.Create, FileAccess.Write)) {
var m2 = typeof(UndertaleIO).GetMethod("Write", new[] { typeof(Stream), typeof(UndertaleData) });
if (m2 != null) m2.Invoke(null, new object[] { stream, Data });
else {
var m3 = typeof(UndertaleIO).GetMethods().FirstOrDefault(m => m.Name == "Write" && m.GetParameters().Length == 3);
if (m3 != null) m3.Invoke(null, new object[] { stream, Data, null });
else {
var w3 = typeof(UndertaleWriter).GetMethods().FirstOrDefault(m => m.Name == "Write" && m.GetParameters().Length == 3);
w3?.Invoke(null, new object[] { stream, Data, null });
}
}
}
"""
with open("/app/extract.csx", "w", encoding="utf-8") as f: f.write(extract_code)
with open("/app/repack.csx", "w", encoding="utf-8") as f: f.write(repack_code)
generate_csx_scripts()
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
jobs = {}
def get_cli_command():
paths = glob.glob("/app/umt/**/UndertaleModCli*", recursive=True) + glob.glob("/app/umt/**/UTMT*", recursive=True)
for p in paths:
if os.path.isfile(p):
if p.endswith(".dll"): return ["dotnet", p]
elif not p.endswith(".exe"): return [p]
return ["/app/umt/UTMT_CLI"]
def deep_heal_droid(filepath):
try:
with open(filepath, "rb") as f: data = bytearray(f.read())
original_header = data[0:4]
if original_header != b'FORM': data[0:4] = b'FORM'
offset = 8
file_len = len(data)
log = []
while offset < file_len:
if offset + 8 > file_len:
data = data[:offset]
break
chunk_name = data[offset:offset+4]
chunk_size = struct.unpack("<I", data[offset+4:offset+8])[0]
is_valid = all(32 <= b <= 126 for b in chunk_name)
if not is_valid or chunk_size > file_len:
log.append(f"TUZAK KESİLDİ: Bozuk Blok '{chunk_name}' | Konum: {offset}")
data = data[:offset]
break
offset += 8 + chunk_size
data[4:8] = struct.pack("<I", len(data) - 8)
with open(filepath, "wb") as f: f.write(data)
return original_header, "\n".join(log)
except Exception as e:
return None, f"İyileştirme Hatası: {str(e)}"
def revert_header(filepath, original_header):
if not original_header or original_header == b'FORM': return
try:
with open(filepath, "rb") as f: data = bytearray(f.read())
data[0:4] = original_header
with open(filepath, "wb") as f: f.write(data)
except: pass
@app.post("/extract")
async def extract_dialogues(file: UploadFile = File(...)):
temp_dir = tempfile.mkdtemp()
asset_path = os.path.join(temp_dir, file.filename)
with open(asset_path, "wb") as f: f.write(await file.read())
fake_header, heal_log = deep_heal_droid(asset_path)
cmd = get_cli_command() + ["load", file.filename, "-s", "/app/extract.csx"]
process = subprocess.run(cmd, cwd=temp_dir, capture_output=True, text=True)
json_path = os.path.join(temp_dir, "strings.json")
zip_filename = os.path.join(temp_dir, f"{file.filename}_dialogues.zip")
with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
if os.path.exists(json_path):
zipf.write(json_path, arcname="strings.json")
# FONT KLASÖRÜNÜ ZİPE EKLE (GARANTİLİ)
font_dir = os.path.join(temp_dir, "Fonts")
if os.path.exists(font_dir):
for root, dirs, files in os.walk(font_dir):
for f in files:
file_path = os.path.join(root, f)
arcname = os.path.relpath(file_path, temp_dir)
zipf.write(file_path, arcname=arcname)
if heal_log: zipf.writestr("IYILESME_RAPORU.txt", f"--- OTO-SIFACI RAPORU ---\n\n{heal_log}".encode('utf-8'))
else:
log_msg = f"=== UMT MOTORU ÇÖKME LOGU ===\nBASLIK: {fake_header}\nSTDOUT:\n{process.stdout}\nSTDERR:\n{process.stderr}\n"
zipf.writestr("UMT_Hata_Logu.txt", log_msg.encode('utf-8'))
return FileResponse(zip_filename, media_type="application/zip", filename=f"{file.filename}_dialogues.zip")
def process_repack(job_id: str, asset_path: str, zip_path: str, original_filename: str):
try:
temp_dir = os.path.dirname(asset_path)
jobs[job_id] = {"status": "processing", "progress": 20, "message": "Oyun korumaları kırılıyor..."}
original_header, heal_log = deep_heal_droid(asset_path)
with zipfile.ZipFile(zip_path, 'r') as zipf:
zipf.extractall(temp_dir)
jobs[job_id].update({"progress": 50, "message": "UMT C# Motoru çalıştırılıyor... Lütfen bekleyin."})
cmd = get_cli_command() + ["load", original_filename, "-s", "/app/repack.csx"]
process = subprocess.run(cmd, cwd=temp_dir, capture_output=True, text=True)
output_path = os.path.join(temp_dir, "repacked_game.droid")
if os.path.exists(output_path):
revert_header(output_path, original_header)
jobs[job_id].update({"status": "completed", "progress": 100, "message": "UMT İşlemi Başarılı! İndiriliyor...", "file_path": output_path, "filename": original_filename})
else:
jobs[job_id].update({"status": "error", "message": f"Paketleme başarısız! Log: {process.stderr}\n{process.stdout}"})
except Exception as e:
jobs[job_id].update({"status": "error", "message": f"Kritik Hata: {str(e)}"})
@app.post("/repack_async")
async def repack_async(background_tasks: BackgroundTasks, asset_file: UploadFile = File(...), mod_zip: UploadFile = File(...)):
job_id = str(uuid.uuid4())
temp_dir = tempfile.mkdtemp()
asset_path = os.path.join(temp_dir, asset_file.filename)
zip_path = os.path.join(temp_dir, mod_zip.filename)
with open(asset_path, "wb") as f: f.write(await asset_file.read())
with open(zip_path, "wb") as f: f.write(await mod_zip.read())
background_tasks.add_task(process_repack, job_id, asset_path, zip_path, asset_file.filename)
jobs[job_id] = {"status": "queued", "progress": 0, "message": "GameMaker işleme sırasına alındı..."}
return {"job_id": job_id}
@app.get("/status/{job_id}")
async def get_status(job_id: str): return jobs.get(job_id, JSONResponse(status_code=404, content={"error": "Bulunamadı"}))
@app.get("/download/{job_id}")
async def download_file(job_id: str): return FileResponse(jobs[job_id]["file_path"], media_type="application/octet-stream", filename=jobs[job_id]["filename"])