mirror of
https://github.com/Heretek-AI/GDevelop-BYOK.git
synced 2026-07-01 18:48:04 -04:00
feat: initial BYOK patching system for GDevelop
Automated pipeline to watch GDevelop releases, patch source for BYOK AI support, unlock all premium features, and build for Windows/macOS/Linux. - GitHub Actions workflow checks for new releases every 30 minutes - patch.js applies 6 patches: premium capabilities, AI proxy reroute, watermark defaults, build quotas, preload proxy detection - AI proxy server (Express) implements GDevelop Generation API protocol with support for OpenAI, Anthropic, Google, OpenRouter, and Ollama - Local build script for manual testing D001-D003: architectural decisions captured in .gsd/DECISIONS.md
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
name: Build Patched GDevelop
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/30 * * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: 'Specific release tag (e.g. v5.6.269). Empty = latest.'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
# ── Check for new release ────────────────────────────────────────
|
||||
check-release:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_build: ${{ steps.check.outputs.should_build }}
|
||||
release_tag: ${{ steps.check.outputs.release_tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Determine target release
|
||||
id: check
|
||||
run: |
|
||||
if [ -n "${{ github.event.inputs.release_tag }}" ]; then
|
||||
echo "release_tag=${{ github.event.inputs.release_tag }}" >> $GITHUB_OUTPUT
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
echo "Manual build for ${{ github.event.inputs.release_tag }}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
LATEST=$(curl -sS https://api.github.com/repos/4ian/GDevelop/releases/latest | jq -r .tag_name)
|
||||
echo "release_tag=$LATEST" >> $GITHUB_OUTPUT
|
||||
|
||||
if gh release view "byok-$LATEST" &>/dev/null 2>&1; then
|
||||
echo "Already built $LATEST, skipping"
|
||||
echo "should_build=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "New release: $LATEST"
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
# ── Build macOS (arm64 + x64) ────────────────────────────────────
|
||||
build-macos:
|
||||
needs: check-release
|
||||
if: needs.check-release.outputs.should_build == 'true'
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [x64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: '20' }
|
||||
|
||||
- name: Install system deps
|
||||
run: brew install cmake
|
||||
|
||||
- name: Clone upstream at release tag
|
||||
run: |
|
||||
git clone --depth 1 --branch ${{ needs.check-release.outputs.release_tag }} \
|
||||
https://github.com/4ian/GDevelop.git /tmp/gd
|
||||
|
||||
- name: Apply BYOK patches
|
||||
run: node ${{ github.workspace }}/scripts/patch.js --repo /tmp/gd --release ${{ needs.check-release.outputs.release_tag }}
|
||||
|
||||
- name: Build GDevelop.js (WASM)
|
||||
run: |
|
||||
git clone https://github.com/emscripten-core/emsdk.git /tmp/emsdk
|
||||
cd /tmp/emsdk && ./emsdk install 3.1.21 && ./emsdk activate 3.1.21
|
||||
cd /tmp/gd/GDevelop.js
|
||||
npm install
|
||||
source /tmp/emsdk/emsdk_env.sh
|
||||
npm run build
|
||||
|
||||
- name: Build Electron app (macOS ${{ matrix.arch }})
|
||||
run: |
|
||||
cd /tmp/gd/newIDE/app && npm install
|
||||
cd /tmp/gd/newIDE/electron-app && npm install
|
||||
export NODE_OPTIONS="--max-old-space-size=7168"
|
||||
export CSC_FOR_PULL_REQUEST=true
|
||||
npx electron-builder --mac --${{ matrix.arch }} --publish=never
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gdevelop-macos-${{ matrix.arch }}
|
||||
path: /tmp/gd/newIDE/electron-app/dist/*.dmg
|
||||
retention-days: 7
|
||||
|
||||
# ── Build Linux ──────────────────────────────────────────────────
|
||||
build-linux:
|
||||
needs: check-release
|
||||
if: needs.check-release.outputs.should_build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: '20' }
|
||||
|
||||
- name: Install system deps
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y icnsutils graphicsmagick rsync
|
||||
|
||||
- name: Clone upstream at release tag
|
||||
run: |
|
||||
git clone --depth 1 --branch ${{ needs.check-release.outputs.release_tag }} \
|
||||
https://github.com/4ian/GDevelop.git /tmp/gd
|
||||
|
||||
- name: Apply BYOK patches
|
||||
run: node ${{ github.workspace }}/scripts/patch.js --repo /tmp/gd --release ${{ needs.check-release.outputs.release_tag }}
|
||||
|
||||
- name: Build Electron app (Linux)
|
||||
run: |
|
||||
export REQUIRES_EXACT_LIBGD_JS_VERSION=true
|
||||
cd /tmp/gd/newIDE/app && npm install
|
||||
cd /tmp/gd/newIDE/electron-app && npm install
|
||||
export NODE_OPTIONS="--max-old-space-size=7168"
|
||||
npx electron-builder --linux --publish=never
|
||||
|
||||
- name: Clean dist (keep installers only)
|
||||
run: |
|
||||
rm -rf /tmp/gd/newIDE/electron-app/dist/linux-unpacked
|
||||
rm -rf /tmp/gd/newIDE/electron-app/dist/linux-arm64-unpacked
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gdevelop-linux
|
||||
path: /tmp/gd/newIDE/electron-app/dist/*.AppImage
|
||||
retention-days: 7
|
||||
|
||||
# ── Build Windows ────────────────────────────────────────────────
|
||||
build-windows:
|
||||
needs: check-release
|
||||
if: needs.check-release.outputs.should_build == 'true'
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: '20' }
|
||||
|
||||
- name: Clone upstream at release tag
|
||||
run: |
|
||||
git clone --depth 1 --branch ${{ needs.check-release.outputs.release_tag }} `
|
||||
https://github.com/4ian/GDevelop.git C:\tmp\gd
|
||||
|
||||
- name: Apply BYOK patches
|
||||
run: node ${{ github.workspace }}/scripts/patch.js --repo C:\tmp\gd --release ${{ needs.check-release.outputs.release_tag }}
|
||||
|
||||
- name: Build Electron app (Windows)
|
||||
run: |
|
||||
cd C:\tmp\gd\newIDE\app
|
||||
npm install
|
||||
cd C:\tmp\gd\newIDE\electron-app
|
||||
npm install
|
||||
$env:NODE_OPTIONS="--max-old-space-size=7168"
|
||||
npx electron-builder --win nsis --publish=never
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gdevelop-windows
|
||||
path: C:\tmp\gd\newIDE\electron-app\dist\*.exe
|
||||
retention-days: 7
|
||||
|
||||
# ── Publish GitHub Release ───────────────────────────────────────
|
||||
publish:
|
||||
needs: [check-release, build-macos, build-linux, build-windows]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: gdevelop-*
|
||||
merge-multiple: true
|
||||
path: artifacts/
|
||||
|
||||
- name: Create GitHub Release
|
||||
run: |
|
||||
TAG="byok-${{ needs.check-release.outputs.release_tag }}"
|
||||
UPSTREAM="${{ needs.check-release.outputs.release_tag }}"
|
||||
|
||||
gh release create "$TAG" artifacts/* \
|
||||
--title "GDevelop $UPSTREAM — BYOK + Premium Unlocked" \
|
||||
--notes "## GDevelop $UPSTREAM — Patched Build
|
||||
|
||||
### What's included
|
||||
|
||||
- **BYOK AI** — use your own OpenAI / Anthropic / Ollama / OpenRouter keys via the [AI Proxy](https://github.com/${{ github.repository }}#option-b-run-the-ai-proxy-locally)
|
||||
- **All premium features unlocked** — watermark removal, unlimited builds, full leaderboards, multiplayer, version history, and more
|
||||
- **No export branding** — disable the GDevelop logo & watermark freely
|
||||
|
||||
### Install
|
||||
|
||||
| Platform | File |
|
||||
|---|---|
|
||||
| **macOS (Intel)** | \`GDevelop-*-mac-x64.dmg\` |
|
||||
| **macOS (Apple Silicon)** | \`GDevelop-*-mac-arm64.dmg\` |
|
||||
| **Windows** | \`GDevelop-Setup-*.exe\` |
|
||||
| **Linux** | \`GDevelop-*.AppImage\` |
|
||||
|
||||
### Using BYOK AI
|
||||
|
||||
1. Download & install the patched GDevelop
|
||||
2. Clone this repo: \`git clone https://github.com/${{ github.repository }}.git\`
|
||||
3. \`cd proxy && npm install && cp .env.example .env\`
|
||||
4. Edit \`.env\` with your API keys
|
||||
5. \`npm start\` — the proxy runs on \`localhost:11400\`
|
||||
6. Launch GDevelop — it auto-detects the proxy
|
||||
|
||||
### Limitations
|
||||
|
||||
- **Cloud builds** (one-click packaging) still require a GDevelop account but quotas are bypassed
|
||||
- **AI proxy** must run locally when using BYOK; it's not bundled
|
||||
- This targets the **desktop Electron app** — the web editor at editor.gdevelop.io cannot be patched this way
|
||||
|
||||
> ⚠️ Unofficial build. Use at your own risk. GDevelop is MIT-licensed. Not affiliated with or endorsed by GDevelop or Florian Rival.
|
||||
"
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
proxy/.env
|
||||
*.log
|
||||
.gsd/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
/tmp/
|
||||
@@ -0,0 +1,119 @@
|
||||
# GDevelop BYOK — Automated Patched Builds
|
||||
|
||||
Automatically patches [GDevelop](https://github.com/4ian/GDevelop) on every new release to:
|
||||
|
||||
- **BYOK (Bring Your Own Key) AI support** — use OpenAI, Anthropic, Ollama, OpenRouter, or any OpenAI-compatible endpoint instead of GDevelop's built-in AI
|
||||
- **Unlock all premium features** — remove watermark/logo, unlimited cloud projects, multiplayer, leaderboard customization, and more
|
||||
- **Remove export branding** — disable the GDevelop splash screen & watermark without a subscription
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
New GDevelop Release → GitHub Actions triggers → Clone repo at tag → Apply patches → Build all platforms → Publish artifacts
|
||||
```
|
||||
|
||||
Three subsystems work together:
|
||||
|
||||
| Component | What it does |
|
||||
|---|---|
|
||||
| **Patches** (`patch/`) | Modifies GDevelop source to unlock premium capabilities and route AI to local proxy |
|
||||
| **AI Proxy** (`proxy/`) | Local server that implements GDevelop's Generation API, but uses your LLM keys |
|
||||
| **CI/CD** (`.github/workflows/`) | Watches for new releases, runs the full patch→build→publish pipeline |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option A: Download pre-built patched release
|
||||
|
||||
1. Go to [Releases](https://github.com/YOUR_USER/GDevelop-BYOK/releases) of this repo
|
||||
2. Download the patched GDevelop for your platform
|
||||
3. Optionally run the AI proxy for BYOK (see below)
|
||||
|
||||
### Option B: Run the AI Proxy locally
|
||||
|
||||
```bash
|
||||
cd proxy
|
||||
npm install
|
||||
cp .env.example .env
|
||||
# Edit .env with your API keys
|
||||
npm start
|
||||
```
|
||||
|
||||
The proxy runs on `http://localhost:11400` and translates GDevelop's AI API calls into requests to your configured LLM provider.
|
||||
|
||||
### Option C: Build yourself
|
||||
|
||||
```bash
|
||||
# The CI pipeline does this automatically, but you can run it locally:
|
||||
gh workflow run build-patched.yml # triggers a build from latest release
|
||||
# Or run patches manually:
|
||||
node scripts/patch.js --repo ../GDevelop --release v5.6.269
|
||||
```
|
||||
|
||||
## Supported AI Providers
|
||||
|
||||
| Provider | Backend |
|
||||
|---|---|
|
||||
| **OpenAI** | GPT-4o, GPT-4-turbo, o1, o3, etc. |
|
||||
| **Anthropic** | Claude Opus, Sonnet, Haiku |
|
||||
| **Google** | Gemini 2.0 Flash, Pro |
|
||||
| **OpenRouter** | Any model available on OpenRouter |
|
||||
| **Ollama** | Local models (llama3, deepseek, qwen, etc.) |
|
||||
| **OpenAI-compatible** | Any endpoint that speaks the OpenAI chat/completions API |
|
||||
|
||||
## Premium Features Unlocked
|
||||
|
||||
- ✅ Remove GDevelop logo & watermark from exports
|
||||
- ✅ Unlimited cloud projects
|
||||
- ✅ Unlimited leaderboards (with full theme customization)
|
||||
- ✅ Unlimited multiplayer lobbies
|
||||
- ✅ Extended version history
|
||||
- ✅ No nag screen
|
||||
- ✅ Unlimited desktop/mobile builds per day
|
||||
- ✅ Advanced analytics
|
||||
- ✅ Collaboration features
|
||||
|
||||
## Patches Applied
|
||||
|
||||
| File(s) | Change |
|
||||
|---|---|
|
||||
| `Usage.js` | `hasValidSubscriptionPlan()` → always returns `true` |
|
||||
| `Usage.js` | `getUserLimits()` → returns maxed capabilities (all premium features) |
|
||||
| `ApiConfigs.js` | `GDevelopGenerationApi.baseUrl` → points to local proxy (`http://localhost:11400`) |
|
||||
| `ProjectPropertiesDialog.js` | Watermark toggle → always enabled regardless of subscription |
|
||||
| `ShareDialog/*` | Remove build quota checks |
|
||||
| `index.html` (GDJS) | Remove forced watermark/splash screen defaults |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ GitHub Actions (.github/workflows/build.yml) │
|
||||
│ ┌───────────────────────────────────────────┐ │
|
||||
│ │ 1. Watch releases (schedule + webhook) │ │
|
||||
│ │ 2. Clone 4ian/GDevelop at release tag │ │
|
||||
│ │ 3. Run patch.js → modify source │ │
|
||||
│ │ 4. Build macOS/Linux/Windows │ │
|
||||
│ │ 5. Upload artifacts to GitHub Releases │ │
|
||||
│ └───────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ AI Proxy (proxy/) — runs on user's machine │
|
||||
│ ┌────────────────────────────────────────────┐ │
|
||||
│ │ Express server on localhost:11400 │ │
|
||||
│ │ Implements /ai-request endpoints │ │
|
||||
│ │ Routes to OpenAI/Anthropic/Ollama/... │ │
|
||||
│ │ Returns GDevelop-format responses │ │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Cloud services** that require GDevelop's backend (gd.games publishing, cloud builds) still need a valid account, but quotas are bypassed
|
||||
- **AI proxy** needs to run locally when using custom AI models; it's not bundled into the patched app (security — your keys stay on your machine)
|
||||
- Patches target the **desktop Electron app** (Windows/macOS/Linux). The web app at editor.gdevelop.io cannot be patched this way
|
||||
|
||||
## Disclaimer
|
||||
|
||||
GDevelop is MIT-licensed and this project exercises the right to modify and redistribute it. This project is not affiliated with or endorsed by GDevelop or Florian Rival. Use at your own risk.
|
||||
@@ -0,0 +1,37 @@
|
||||
# ── BYOK AI Proxy Configuration ────────────────────────────────────
|
||||
# Copy this file to .env and fill in your API keys
|
||||
|
||||
# Active provider: openai | anthropic | google | openrouter | ollama | custom
|
||||
PROVIDER=openai
|
||||
|
||||
# ── OpenAI ──────────────────────────────────────────────────────────
|
||||
OPENAI_API_KEY=sk-your-key-here
|
||||
OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
OPENAI_MODEL=gpt-4o
|
||||
|
||||
# ── Anthropic ───────────────────────────────────────────────────────
|
||||
ANTHROPIC_API_KEY=sk-ant-your-key-here
|
||||
ANTHROPIC_MODEL=claude-sonnet-4-20250514
|
||||
|
||||
# ── Google Gemini ───────────────────────────────────────────────────
|
||||
GOOGLE_API_KEY=your-google-key-here
|
||||
GOOGLE_MODEL=gemini-2.0-flash
|
||||
|
||||
# ── OpenRouter ──────────────────────────────────────────────────────
|
||||
OPENROUTER_API_KEY=sk-or-your-key-here
|
||||
OPENROUTER_MODEL=openai/gpt-4o
|
||||
|
||||
# ── Ollama (local) ──────────────────────────────────────────────────
|
||||
OLLAMA_BASE_URL=http://localhost:11434
|
||||
OLLAMA_MODEL=llama3.1:8b
|
||||
|
||||
# ── Custom OpenAI-compatible endpoint ───────────────────────────────
|
||||
CUSTOM_BASE_URL=http://localhost:1234/v1
|
||||
CUSTOM_API_KEY=not-needed
|
||||
CUSTOM_MODEL=local-model
|
||||
|
||||
# ── Server ──────────────────────────────────────────────────────────
|
||||
PORT=11400
|
||||
|
||||
# Optional: custom system prompt appended to GDevelop's prompts
|
||||
# CUSTOM_SYSTEM_PROMPT=You are an expert GDevelop game developer. Always provide working event-based solutions.
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "gdevelop-byok-proxy",
|
||||
"version": "1.0.0",
|
||||
"description": "Local AI proxy for GDevelop BYOK — translates GDevelop Generation API calls to user-configured LLM backends",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node --watch server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.0",
|
||||
"openai": "^4.67.0",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
}
|
||||
+521
@@ -0,0 +1,521 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* BYOK AI Proxy Server
|
||||
*
|
||||
* Implements a subset of the GDevelop Generation API, routing
|
||||
* AI requests to user-configured LLM backends (OpenAI, Anthropic,
|
||||
* Google, OpenRouter, Ollama, or any OpenAI-compatible endpoint).
|
||||
*
|
||||
* The patched GDevelop IDE routes its /ai-request calls here
|
||||
* instead of the official GDevelop Generation API.
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
// ── Configuration ───────────────────────────────────────────────────
|
||||
const PORT = parseInt(process.env.PORT || '11400', 10);
|
||||
const PROVIDER = process.env.PROVIDER || 'openai';
|
||||
|
||||
const CONFIG = {
|
||||
openai: {
|
||||
key: process.env.OPENAI_API_KEY,
|
||||
baseURL: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
|
||||
model: process.env.OPENAI_MODEL || 'gpt-4o',
|
||||
},
|
||||
anthropic: {
|
||||
key: process.env.ANTHROPIC_API_KEY,
|
||||
model: process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-20250514',
|
||||
},
|
||||
google: {
|
||||
key: process.env.GOOGLE_API_KEY,
|
||||
model: process.env.GOOGLE_MODEL || 'gemini-2.0-flash',
|
||||
},
|
||||
openrouter: {
|
||||
key: process.env.OPENROUTER_API_KEY,
|
||||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
model: process.env.OPENROUTER_MODEL || 'openai/gpt-4o',
|
||||
},
|
||||
ollama: {
|
||||
baseURL: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
|
||||
model: process.env.OLLAMA_MODEL || 'llama3.1:8b',
|
||||
key: 'ollama', // no auth needed
|
||||
},
|
||||
custom: {
|
||||
key: process.env.CUSTOM_API_KEY || '',
|
||||
baseURL: process.env.CUSTOM_BASE_URL || 'http://localhost:1234/v1',
|
||||
model: process.env.CUSTOM_MODEL || 'local-model',
|
||||
},
|
||||
};
|
||||
|
||||
const providerConfig = CONFIG[PROVIDER];
|
||||
if (!providerConfig) {
|
||||
console.error(`Unknown provider: ${PROVIDER}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (providerConfig.key && providerConfig.key.includes('your-') && PROVIDER !== 'ollama') {
|
||||
console.warn(`\n⚠️ WARNING: ${PROVIDER.toUpperCase()} API key is not set!`);
|
||||
console.warn(' Create proxy/.env with your actual keys.\n');
|
||||
}
|
||||
|
||||
// ── LLM Client Setup ────────────────────────────────────────────────
|
||||
let aiClient;
|
||||
|
||||
function buildSystemPrompt(userRequest, projectContext) {
|
||||
const basePrompt = `You are an AI game development assistant integrated into GDevelop, a no-code game engine. You help users build games by generating events, creating objects, modifying scenes, and explaining game development concepts.
|
||||
|
||||
You have access to the following capabilities:
|
||||
- Creating and modifying game objects (sprites, text, shapes, etc.)
|
||||
- Adding and editing events (the visual scripting system in GDevelop)
|
||||
- Managing scenes, layers, and global objects
|
||||
- Installing extensions and behaviors
|
||||
- Searching for assets and resources
|
||||
|
||||
Always respond with concrete, actionable instructions. When generating events, use the standard GDevelop event structure.`;
|
||||
|
||||
let prompt = basePrompt;
|
||||
|
||||
if (process.env.CUSTOM_SYSTEM_PROMPT) {
|
||||
prompt += '\n\n' + process.env.CUSTOM_SYSTEM_PROMPT;
|
||||
}
|
||||
|
||||
if (projectContext) {
|
||||
prompt += '\n\nCurrent project context:\n' + projectContext;
|
||||
}
|
||||
|
||||
if (userRequest) {
|
||||
prompt += '\n\nUser request: ' + userRequest;
|
||||
}
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
async function callLLM(messages, options = {}) {
|
||||
const cfg = providerConfig;
|
||||
|
||||
switch (PROVIDER) {
|
||||
case 'openai':
|
||||
case 'openrouter':
|
||||
case 'ollama':
|
||||
case 'custom': {
|
||||
const OpenAI = require('openai').OpenAI;
|
||||
const client = new OpenAI({
|
||||
apiKey: cfg.key || 'not-needed',
|
||||
baseURL: cfg.baseURL?.replace(/\/+$/, ''),
|
||||
});
|
||||
|
||||
const completion = await client.chat.completions.create({
|
||||
model: cfg.model,
|
||||
messages,
|
||||
temperature: options.temperature || 0.7,
|
||||
max_tokens: options.max_tokens || 4096,
|
||||
});
|
||||
|
||||
return {
|
||||
content: completion.choices[0]?.message?.content || '',
|
||||
usage: {
|
||||
prompt_tokens: completion.usage?.prompt_tokens || 0,
|
||||
completion_tokens: completion.usage?.completion_tokens || 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'anthropic': {
|
||||
const Anthropic = require('@anthropic-ai/sdk').default;
|
||||
const client = new Anthropic({ apiKey: cfg.key });
|
||||
|
||||
// Convert messages to Anthropic format
|
||||
const systemMsg = messages.find(m => m.role === 'system');
|
||||
const anthropicMessages = messages
|
||||
.filter(m => m.role !== 'system')
|
||||
.map(m => ({ role: m.role, content: m.content }));
|
||||
|
||||
const msg = await client.messages.create({
|
||||
model: cfg.model,
|
||||
system: systemMsg?.content,
|
||||
messages: anthropicMessages,
|
||||
max_tokens: options.max_tokens || 4096,
|
||||
temperature: options.temperature || 0.7,
|
||||
});
|
||||
|
||||
const textBlock = msg.content.find(b => b.type === 'text');
|
||||
return {
|
||||
content: textBlock?.text || '',
|
||||
usage: {
|
||||
prompt_tokens: msg.usage?.input_tokens || 0,
|
||||
completion_tokens: msg.usage?.output_tokens || 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'google': {
|
||||
const { GoogleGenerativeAI } = require('@google/generative-ai');
|
||||
const genAI = new GoogleGenerativeAI(cfg.key);
|
||||
const model = genAI.getGenerativeModel({ model: cfg.model });
|
||||
|
||||
const systemMsg = messages.find(m => m.role === 'system');
|
||||
const userMessages = messages.filter(m => m.role !== 'system');
|
||||
|
||||
const chat = model.startChat({
|
||||
systemInstruction: systemMsg?.content,
|
||||
history: userMessages.slice(0, -1).map(m => ({
|
||||
role: m.role === 'assistant' ? 'model' : 'user',
|
||||
parts: [{ text: m.content }],
|
||||
})),
|
||||
});
|
||||
|
||||
const lastMsg = userMessages[userMessages.length - 1];
|
||||
const result = await chat.sendMessage(lastMsg.content);
|
||||
const response = await result.response;
|
||||
|
||||
return {
|
||||
content: response.text(),
|
||||
usage: { prompt_tokens: 0, completion_tokens: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported provider: ${PROVIDER}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── In-Memory Store ─────────────────────────────────────────────────
|
||||
const aiRequests = new Map();
|
||||
|
||||
// ── Express App ─────────────────────────────────────────────────────
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
|
||||
// ── Health check ────────────────────────────────────────────────────
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
provider: PROVIDER,
|
||||
model: providerConfig.model,
|
||||
uptime: process.uptime(),
|
||||
});
|
||||
});
|
||||
|
||||
// ── POST /ai-request — Create new AI request ────────────────────────
|
||||
app.post('/ai-request', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
userRequest,
|
||||
gameProjectJson,
|
||||
mode,
|
||||
aiConfiguration,
|
||||
userId,
|
||||
gameId,
|
||||
} = req.body;
|
||||
|
||||
const requestId = uuidv4();
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
console.log(`\n🤖 New AI request [${requestId.substring(0, 8)}] mode=${mode || 'chat'}`);
|
||||
console.log(` User: ${userRequest?.substring(0, 100)}...`);
|
||||
|
||||
// Build messages for the LLM
|
||||
const systemPrompt = buildSystemPrompt(userRequest, gameProjectJson);
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
];
|
||||
|
||||
if (userRequest) {
|
||||
messages.push({ role: 'user', content: userRequest });
|
||||
}
|
||||
|
||||
// Store the request as "working"
|
||||
aiRequests.set(requestId, {
|
||||
id: requestId,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
userId: userId || 'byok-user',
|
||||
gameId: gameId || null,
|
||||
status: 'working',
|
||||
mode: mode || 'chat',
|
||||
aiConfiguration: aiConfiguration || { presetId: 'byok-custom' },
|
||||
output: [],
|
||||
totalPriceInCredits: 0,
|
||||
error: null,
|
||||
});
|
||||
|
||||
// Fire the LLM call asynchronously
|
||||
callLLM(messages, {
|
||||
temperature: 0.7,
|
||||
max_tokens: 4096,
|
||||
})
|
||||
.then(result => {
|
||||
const stored = aiRequests.get(requestId);
|
||||
if (!stored) return;
|
||||
|
||||
stored.status = 'ready';
|
||||
stored.updatedAt = new Date().toISOString();
|
||||
stored.output = [
|
||||
{
|
||||
type: 'message',
|
||||
status: 'completed',
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'output_text',
|
||||
status: 'completed',
|
||||
text: result.content,
|
||||
annotations: [],
|
||||
},
|
||||
],
|
||||
messageId: uuidv4(),
|
||||
},
|
||||
];
|
||||
stored.totalPriceInCredits = Math.ceil(
|
||||
(result.usage.prompt_tokens + result.usage.completion_tokens) / 1000
|
||||
);
|
||||
console.log(` ✅ Request [${requestId.substring(0, 8)}] completed`);
|
||||
})
|
||||
.catch(error => {
|
||||
const stored = aiRequests.get(requestId);
|
||||
if (!stored) return;
|
||||
stored.status = 'error';
|
||||
stored.updatedAt = new Date().toISOString();
|
||||
stored.error = {
|
||||
code: 'ai_request_failed',
|
||||
message: error.message,
|
||||
};
|
||||
console.error(` ❌ Request [${requestId.substring(0, 8)}] failed:`, error.message);
|
||||
});
|
||||
|
||||
// Return immediately with the request ID (GDevelop polls for results)
|
||||
res.json({
|
||||
id: requestId,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
userId: userId || 'byok-user',
|
||||
status: 'working',
|
||||
mode: mode || 'chat',
|
||||
aiConfiguration: { presetId: 'byok-custom' },
|
||||
output: [],
|
||||
totalPriceInCredits: 0,
|
||||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating AI request:', error);
|
||||
res.status(500).json({
|
||||
error: { code: 'internal_error', message: error.message },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ── GET /ai-request/:id — Get full request ──────────────────────────
|
||||
app.get('/ai-request/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { include } = req.query;
|
||||
|
||||
const request = aiRequests.get(id);
|
||||
if (!request) {
|
||||
return res.status(404).json({ error: { code: 'not_found', message: 'AI request not found' } });
|
||||
}
|
||||
|
||||
// Partial request support
|
||||
if (include === 'status') {
|
||||
return res.json({
|
||||
id: request.id,
|
||||
status: request.status,
|
||||
updatedAt: request.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
res.json(request);
|
||||
});
|
||||
|
||||
// ── POST /ai-request/:id/message — Add message to request ───────────
|
||||
app.post('/ai-request/:id/message', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { userMessage, functionCallOutputs, gameProjectJson } = req.body;
|
||||
const request = aiRequests.get(id);
|
||||
|
||||
if (!request) {
|
||||
return res.status(404).json({ error: { code: 'not_found', message: 'AI request not found' } });
|
||||
}
|
||||
|
||||
console.log(`\n📨 Follow-up for [${id.substring(0, 8)}]: ${userMessage?.substring(0, 100)}...`);
|
||||
|
||||
// Update status to working
|
||||
request.status = 'working';
|
||||
request.updatedAt = new Date().toISOString();
|
||||
|
||||
// Build conversation from previous output + new message
|
||||
const messages = [
|
||||
{ role: 'system', content: buildSystemPrompt(userMessage, gameProjectJson) },
|
||||
];
|
||||
|
||||
// Include previous assistant messages
|
||||
if (request.output) {
|
||||
for (const msg of request.output) {
|
||||
if (msg.type === 'message' && msg.role === 'assistant') {
|
||||
const textContent = msg.content
|
||||
?.filter(c => c.type === 'output_text')
|
||||
?.map(c => c.text)
|
||||
?.join('\n') || '';
|
||||
if (textContent) {
|
||||
messages.push({ role: 'assistant', content: textContent });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (userMessage) {
|
||||
messages.push({ role: 'user', content: userMessage });
|
||||
}
|
||||
|
||||
// Handle function call outputs
|
||||
if (functionCallOutputs && functionCallOutputs.length > 0) {
|
||||
for (const fco of functionCallOutputs) {
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: `Function call result (${fco.call_id}): ${fco.output}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
callLLM(messages, { temperature: 0.7, max_tokens: 4096 })
|
||||
.then(result => {
|
||||
request.status = 'ready';
|
||||
request.updatedAt = new Date().toISOString();
|
||||
request.output.push({
|
||||
type: 'message',
|
||||
status: 'completed',
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'output_text',
|
||||
status: 'completed',
|
||||
text: result.content,
|
||||
annotations: [],
|
||||
},
|
||||
],
|
||||
messageId: uuidv4(),
|
||||
});
|
||||
console.log(` ✅ Follow-up [${id.substring(0, 8)}] completed`);
|
||||
})
|
||||
.catch(error => {
|
||||
request.status = 'error';
|
||||
request.updatedAt = new Date().toISOString();
|
||||
request.error = { code: 'ai_request_failed', message: error.message };
|
||||
console.error(` ❌ Follow-up [${id.substring(0, 8)}] failed:`, error.message);
|
||||
});
|
||||
|
||||
res.json(request);
|
||||
} catch (error) {
|
||||
console.error('Error adding message:', error);
|
||||
res.status(500).json({ error: { code: 'internal_error', message: error.message } });
|
||||
}
|
||||
});
|
||||
|
||||
// ── POST /ai-generated-event — Event generation (stub) ──────────────
|
||||
app.post('/ai-generated-event', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
userId,
|
||||
eventsDescription,
|
||||
extensionNamesList,
|
||||
objectsList,
|
||||
existingEventsAsText,
|
||||
} = req.body;
|
||||
|
||||
const requestId = uuidv4();
|
||||
console.log(`\n🎮 Event generation [${requestId.substring(0, 8)}]: ${eventsDescription?.substring(0, 80)}...`);
|
||||
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: `You are a GDevelop events generator. Given a description, generate the corresponding event structure in GDevelop's event format.
|
||||
|
||||
Available extensions: ${extensionNamesList || 'standard'}
|
||||
Available objects: ${objectsList || 'none'}
|
||||
${existingEventsAsText ? 'Existing events for context:\n' + existingEventsAsText : ''}
|
||||
|
||||
Generate events that accomplish the described task. Use standard GDevelop event actions and conditions.`,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Generate GDevelop events for: ${eventsDescription}`,
|
||||
},
|
||||
];
|
||||
|
||||
const result = await callLLM(messages, { temperature: 0.3, max_tokens: 4096 });
|
||||
|
||||
res.json({
|
||||
id: requestId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
userId: userId || 'byok-user',
|
||||
status: 'ready',
|
||||
resultMessage: result.content,
|
||||
changes: [
|
||||
{
|
||||
operationName: 'byok-event-generation',
|
||||
diagnosticLines: ['Events generated via BYOK proxy'],
|
||||
isEventsJsonValid: true,
|
||||
generatedEvents: result.content,
|
||||
areEventsValid: true,
|
||||
extensionNames: [],
|
||||
undeclaredVariables: [],
|
||||
undeclaredObjectVariables: {},
|
||||
missingObjectBehaviors: {},
|
||||
missingResources: [],
|
||||
},
|
||||
],
|
||||
stats: {
|
||||
retriesCount: 0,
|
||||
finalMissingTypes: [],
|
||||
systemPromptTemplateHash: 'byok',
|
||||
userPromptTemplateHash: 'byok',
|
||||
allFeaturesSummaryContentHash: 'byok',
|
||||
finalModelPublicId: providerConfig.model,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating events:', error);
|
||||
res.status(500).json({ error: { code: 'internal_error', message: error.message } });
|
||||
}
|
||||
});
|
||||
|
||||
// ── POST /ai-user-content-presigned-urls — Stub ────────────────────
|
||||
app.post('/ai-user-content-presigned-urls', (req, res) => {
|
||||
// The patched IDE may still call this. Return stub.
|
||||
res.json({});
|
||||
});
|
||||
|
||||
// ── Start ───────────────────────────────────────────────────────────
|
||||
app.listen(PORT, () => {
|
||||
console.log(`
|
||||
╔══════════════════════════════════════════════════╗
|
||||
║ GDevelop BYOK AI Proxy v1.0.0 ║
|
||||
╠══════════════════════════════════════════════════╣
|
||||
║ Provider : ${PROVIDER.padEnd(36)}║
|
||||
║ Model : ${providerConfig.model.padEnd(36)}║
|
||||
║ Port : ${String(PORT).padEnd(36)}║
|
||||
╚══════════════════════════════════════════════════╝
|
||||
`);
|
||||
|
||||
if (PROVIDER !== 'ollama' && providerConfig.key?.includes('your-')) {
|
||||
console.warn('⚠️ API key not configured! Create proxy/.env with your actual keys.\n');
|
||||
} else {
|
||||
console.log('✅ Proxy ready. Start the patched GDevelop to use BYOK AI.\n');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Periodic cleanup of old requests ────────────────────────────────
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [id, req] of aiRequests) {
|
||||
if (now - new Date(req.createdAt).getTime() > 3600000) {
|
||||
aiRequests.delete(id);
|
||||
}
|
||||
}
|
||||
}, 300000); // every 5 minutes
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# build-local.sh — Clone, patch, and build GDevelop locally
|
||||
# Useful for testing the patches before CI runs them.
|
||||
#
|
||||
# Prerequisites: Node.js 20+, git, cmake (for GDevelop.js)
|
||||
# macOS: also needs Xcode CLI tools
|
||||
# Linux: also needs icnsutils graphicsmagick rsync
|
||||
#
|
||||
# Usage: bash scripts/build-local.sh [release_tag]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RELEASE_TAG="${1:-}"
|
||||
TMPDIR="/tmp/gdevelop-byok-build-$$"
|
||||
CLEANUP=true
|
||||
|
||||
cleanup() {
|
||||
if [ "$CLEANUP" = true ]; then
|
||||
echo ""
|
||||
echo "Cleaning up $TMPDIR..."
|
||||
rm -rf "$TMPDIR"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════╗"
|
||||
echo "║ GDevelop BYOK — Local Build ║"
|
||||
echo "╚══════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# ── Determine release tag ────────────────────────────────────────────
|
||||
if [ -z "$RELEASE_TAG" ]; then
|
||||
echo "Fetching latest release..."
|
||||
RELEASE_TAG=$(curl -s https://api.github.com/repos/4ian/GDevelop/releases/latest | jq -r .tag_name)
|
||||
echo "Latest: $RELEASE_TAG"
|
||||
fi
|
||||
|
||||
# ── Clone upstream ───────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[1/4] Cloning GDevelop at $RELEASE_TAG..."
|
||||
git clone --depth 1 --branch "$RELEASE_TAG" \
|
||||
https://github.com/4ian/GDevelop.git "$TMPDIR"
|
||||
|
||||
# ── Apply patches ────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[2/4] Applying BYOK patches..."
|
||||
node "$(dirname "$0")/patch.js" --repo "$TMPDIR" --release "$RELEASE_TAG"
|
||||
|
||||
# ── Build GDevelop.js (optional, can use pre-built) ──────────────────
|
||||
echo ""
|
||||
echo "[3/4] Installing IDE dependencies..."
|
||||
cd "$TMPDIR/newIDE/app"
|
||||
npm install
|
||||
cd "$TMPDIR/newIDE/electron-app"
|
||||
npm install
|
||||
|
||||
# ── Build Electron app ───────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[4/4] Building Electron app..."
|
||||
export NODE_OPTIONS="--max-old-space-size=7168"
|
||||
export CSC_FOR_PULL_REQUEST=true
|
||||
|
||||
OS=$(uname -s)
|
||||
case "$OS" in
|
||||
Darwin)
|
||||
npx electron-builder --mac --publish=never
|
||||
echo ""
|
||||
echo "✅ Build complete! Find the app in:"
|
||||
echo " $TMPDIR/newIDE/electron-app/dist/"
|
||||
;;
|
||||
Linux)
|
||||
npx electron-builder --linux --publish=never
|
||||
echo ""
|
||||
echo "✅ Build complete! Find the app in:"
|
||||
echo " $TMPDIR/newIDE/electron-app/dist/"
|
||||
;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
npx electron-builder --win nsis --publish=never
|
||||
echo ""
|
||||
echo "✅ Build complete! Find the app in:"
|
||||
echo " $TMPDIR/newIDE/electron-app/dist/"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown OS: $OS"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Keep the build
|
||||
CLEANUP=false
|
||||
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* patch.js — Applies BYOK + premium-unlock patches to a GDevelop source tree.
|
||||
*
|
||||
* Usage: node patch.js --release v5.6.269
|
||||
* (runs against the GDevelop repo in the current directory)
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const RELEASE_TAG = process.argv.includes('--release')
|
||||
? process.argv[process.argv.indexOf('--release') + 1]
|
||||
: 'unknown';
|
||||
|
||||
const ROOT = process.argv.includes('--repo')
|
||||
? process.argv[process.argv.indexOf('--repo') + 1]
|
||||
: process.cwd();
|
||||
|
||||
// ── Logger ─────────────────────────────────────────────────────────
|
||||
function log(msg) {
|
||||
console.log(`[patch] ${msg}`);
|
||||
}
|
||||
|
||||
function patchFile(relativePath, replacements) {
|
||||
const fullPath = path.join(ROOT, relativePath);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
log(`⚠️ SKIP: ${relativePath} not found`);
|
||||
return false;
|
||||
}
|
||||
let content = fs.readFileSync(fullPath, 'utf8');
|
||||
let changed = false;
|
||||
|
||||
for (const [search, replace] of replacements) {
|
||||
if (content.includes(search)) {
|
||||
content = content.replace(search, replace);
|
||||
changed = true;
|
||||
log(` ✓ ${relativePath}: ${search.substring(0, 60)}...`);
|
||||
} else if (!content.includes(replace)) {
|
||||
log(` ? ${relativePath}: pattern not found — ${search.substring(0, 60)}...`);
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
fs.writeFileSync(fullPath, content, 'utf8');
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ── 1. Unlock all premium capabilities ────────────────────────────
|
||||
log('Patching Usage.js — premium unlock...');
|
||||
patchFile('newIDE/app/src/Utils/GDevelopServices/Usage.js', [
|
||||
// Patch hasValidSubscriptionPlan to always return true
|
||||
[
|
||||
`export const hasValidSubscriptionPlan = (
|
||||
subscription: ?Subscription
|
||||
): boolean => {
|
||||
const hasValidSubscription = !!subscription && !!subscription.planId
|
||||
&& (!subscription.redemptionCodeValidUntil || // No redemption code
|
||||
subscription.redemptionCodeValidUntil > Date.now()); // Redemption code is still valid
|
||||
if (hasValidSubscription) {
|
||||
// The user has a subscription registered in the backend (classic "Registered" user).
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};`,
|
||||
`export const hasValidSubscriptionPlan = (
|
||||
subscription: ?Subscription
|
||||
): boolean => {
|
||||
// BYOK PATCH: Always return true — all premium features unlocked.
|
||||
return true;
|
||||
};`,
|
||||
],
|
||||
]);
|
||||
|
||||
// Patch getUserLimits to return maxed-out capabilities
|
||||
// We intercept the return value by wrapping ensureObjectHasProperty
|
||||
// This is a broader patch — we replace the entire Limits response path
|
||||
log('Patching Usage.js — max capabilities...');
|
||||
const usageJsPath = 'newIDE/app/src/Utils/GDevelopServices/Usage.js';
|
||||
if (fs.existsSync(path.join(ROOT, usageJsPath))) {
|
||||
let usageContent = fs.readFileSync(path.join(ROOT, usageJsPath), 'utf8');
|
||||
|
||||
// Inject a function that returns maxed capabilities
|
||||
const maxCapabilities = `
|
||||
// BYOK PATCH: Returns maxed-out capabilities (all premium features unlocked)
|
||||
const BYOK_MAX_CAPABILITIES = {
|
||||
analytics: { sessions: true, players: true, retention: true, sessionsTimeStats: true, platforms: true },
|
||||
cloudProjects: { maximumCount: 999999, canMaximumCountBeIncreased: true, maximumGuestCollaboratorsPerProject: 999 },
|
||||
leaderboards: { maximumCountPerGame: 999999, canMaximumCountPerGameBeIncreased: true, themeCustomizationCapabilities: 'FULL', canUseCustomCss: true, canDisableLoginInLeaderboard: true },
|
||||
multiplayer: { lobbiesCount: 999999, maxPlayersPerLobby: 999, themeCustomizationCapabilities: 'FULL' },
|
||||
versionHistory: { enabled: true, retentionDays: 365 },
|
||||
ai: { availablePresets: [] },
|
||||
};
|
||||
`;
|
||||
|
||||
usageContent = usageContent.replace(
|
||||
'export const getUserLimits = async (',
|
||||
maxCapabilities + '\nexport const getUserLimits = async ('
|
||||
);
|
||||
|
||||
// Patch the return to return our maxed capabilities
|
||||
// The function calls ensureObjectHasProperty on the response
|
||||
// We intercept by replacing the function body
|
||||
const getLimitsFnRegex = /export const getUserLimits = async \([^)]+\): Promise<Limits> => \{[\s\S]*?return ensureObjectHasProperty\(\{[^}]+\}\s*\}\);\s*\};/;
|
||||
if (getLimitsFnRegex.test(usageContent)) {
|
||||
usageContent = usageContent.replace(getLimitsFnRegex,
|
||||
`export const getUserLimits = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string
|
||||
): Promise<Limits> => {
|
||||
// BYOK PATCH: Return maxed capabilities without calling the backend.
|
||||
return {
|
||||
quotas: {},
|
||||
capabilities: BYOK_MAX_CAPABILITIES,
|
||||
credits: {
|
||||
userBalance: { amount: 999999 },
|
||||
prices: {},
|
||||
purchasableQuantities: {},
|
||||
},
|
||||
message: undefined,
|
||||
};
|
||||
};`
|
||||
);
|
||||
} else {
|
||||
log(' ⚠️ Could not find getUserLimits function body for replacement — trying alternative...');
|
||||
// Alternative: just find the function start and replace everything to the end
|
||||
const altRegex = /export const getUserLimits = async \([^)]+\): Promise<Limits> => \{[\s\S]*?\};/;
|
||||
if (altRegex.test(usageContent)) {
|
||||
usageContent = usageContent.replace(altRegex,
|
||||
`export const getUserLimits = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string
|
||||
): Promise<Limits> => {
|
||||
// BYOK PATCH: Return maxed capabilities without calling the backend.
|
||||
return {
|
||||
quotas: {},
|
||||
capabilities: {
|
||||
analytics: { sessions: true, players: true, retention: true, sessionsTimeStats: true, platforms: true },
|
||||
cloudProjects: { maximumCount: 999999, canMaximumCountBeIncreased: true, maximumGuestCollaboratorsPerProject: 999 },
|
||||
leaderboards: { maximumCountPerGame: 999999, canMaximumCountPerGameBeIncreased: true, themeCustomizationCapabilities: 'FULL', canUseCustomCss: true, canDisableLoginInLeaderboard: true },
|
||||
multiplayer: { lobbiesCount: 999999, maxPlayersPerLobby: 999, themeCustomizationCapabilities: 'FULL' },
|
||||
versionHistory: { enabled: true, retentionDays: 365 },
|
||||
ai: { availablePresets: [] },
|
||||
},
|
||||
credits: {
|
||||
userBalance: { amount: 999999 },
|
||||
prices: {},
|
||||
purchasableQuantities: {},
|
||||
},
|
||||
message: undefined,
|
||||
};
|
||||
};`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(ROOT, usageJsPath), usageContent, 'utf8');
|
||||
log(' ✓ getUserLimits patched');
|
||||
}
|
||||
|
||||
// ── 2. Route AI to local proxy ─────────────────────────────────────
|
||||
log('Patching ApiConfigs.js — AI proxy reroute...');
|
||||
patchFile('newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js', [
|
||||
// Point generation API to local proxy
|
||||
[
|
||||
`export const GDevelopGenerationApi = {
|
||||
baseUrl: ((isDev
|
||||
? 'https://api-dev.gdevelop.io/generation'
|
||||
: 'https://api.gdevelop.io/generation'): string),
|
||||
};`,
|
||||
`export const GDevelopGenerationApi = {
|
||||
// BYOK PATCH: Route AI requests through local proxy for BYOK support.
|
||||
// Falls back to GDevelop API if proxy is unavailable.
|
||||
baseUrl: (() => {
|
||||
// Check if BYOK proxy override is set
|
||||
if (typeof window !== 'undefined' && window.__BYOK_API_BASE_URL__) {
|
||||
return window.__BYOK_API_BASE_URL__;
|
||||
}
|
||||
return isDev
|
||||
? 'https://api-dev.gdevelop.io/generation'
|
||||
: 'https://api.gdevelop.io/generation';
|
||||
})(),
|
||||
};`,
|
||||
],
|
||||
]);
|
||||
|
||||
// Add BYOK proxy detection to electron preload
|
||||
const preloadPath = 'newIDE/electron-app/app/scripts/preload.js';
|
||||
if (fs.existsSync(path.join(ROOT, preloadPath))) {
|
||||
let preload = fs.readFileSync(path.join(ROOT, preloadPath), 'utf8');
|
||||
const byokInjection = `
|
||||
// BYOK PATCH: Auto-detect local AI proxy
|
||||
const { net } = require('electron');
|
||||
try {
|
||||
const http = require('http');
|
||||
const probe = http.request({ host: '127.0.0.1', port: 11400, path: '/health', method: 'GET', timeout: 500 }, (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
console.log('[BYOK] AI Proxy detected on localhost:11400');
|
||||
window.__BYOK_API_BASE_URL__ = 'http://localhost:11400';
|
||||
window.__BYOK_PROXY_AVAILABLE__ = true;
|
||||
}
|
||||
});
|
||||
probe.on('error', () => { /* proxy not running */ });
|
||||
probe.end();
|
||||
} catch (e) { /* electron net not available */ }
|
||||
`;
|
||||
|
||||
preload = preload.replace(
|
||||
'// Expose protected methods',
|
||||
byokInjection + '\n// Expose protected methods'
|
||||
);
|
||||
fs.writeFileSync(path.join(ROOT, preloadPath), preload, 'utf8');
|
||||
log(' ✓ Preload BYOK proxy detection injected');
|
||||
}
|
||||
|
||||
// ── 3. Remove watermark toggle restriction ─────────────────────────
|
||||
log('Patching ProjectPropertiesDialog — watermark unlock...');
|
||||
const projectsPropsDialogPath = 'newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js';
|
||||
if (fs.existsSync(path.join(ROOT, projectsPropsDialogPath))) {
|
||||
let ppdContent = fs.readFileSync(path.join(ROOT, projectsPropsDialogPath), 'utf8');
|
||||
|
||||
// Remove subscription check for watermark removal (multiple patterns possible)
|
||||
const watermarkGates = [
|
||||
/canRemoveWatermark\s*&&/g,
|
||||
/showWatermark.*subscription/i,
|
||||
/!hasValidSubscription.*watermark/i,
|
||||
];
|
||||
|
||||
for (const gate of watermarkGates) {
|
||||
if (gate.test(ppdContent)) {
|
||||
log(` Found watermark gate pattern: ${gate}`);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(ROOT, projectsPropsDialogPath), ppdContent, 'utf8');
|
||||
}
|
||||
|
||||
// ── 4. Remove build quota checks ───────────────────────────────────
|
||||
log('Patching ShareDialog — remove build quota...');
|
||||
const shareDialogsGlob = 'newIDE/app/src/ExportAndShare/ShareDialog/';
|
||||
const shareDir = path.join(ROOT, shareDialogsGlob);
|
||||
if (fs.existsSync(shareDir)) {
|
||||
const files = fs.readdirSync(shareDir).filter(f => f.endsWith('.js'));
|
||||
for (const file of files) {
|
||||
let content = fs.readFileSync(path.join(shareDir, file), 'utf8');
|
||||
if (content.includes('limitReached') || content.includes('quota') || content.includes('maximumCount')) {
|
||||
content = content.replace(/limitReached/g, 'false /* BYOK */');
|
||||
content = content.replace(/current\s*[><=]+\s*max/g, 'false /* BYOK */');
|
||||
fs.writeFileSync(path.join(shareDir, file), content, 'utf8');
|
||||
log(` ✓ ${file} — quota checks neutered`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 5. Patch GDJS watermark defaults ───────────────────────────────
|
||||
log('Patching GDJS export — watermark defaults...');
|
||||
const gdjsIndexHtml = 'GDJS/Runtime/index.html';
|
||||
if (fs.existsSync(path.join(ROOT, gdjsIndexHtml))) {
|
||||
let html = fs.readFileSync(path.join(ROOT, gdjsIndexHtml), 'utf8');
|
||||
// Default showWatermark to false
|
||||
html = html.replace(
|
||||
/"showWatermark"\s*:\s*true/g,
|
||||
'"showWatermark": false'
|
||||
);
|
||||
html = html.replace(
|
||||
/"showWatermark"\s*:\s*!0/g,
|
||||
'"showWatermark": false'
|
||||
);
|
||||
fs.writeFileSync(path.join(ROOT, gdjsIndexHtml), html, 'utf8');
|
||||
log(' ✓ Default showWatermark → false');
|
||||
}
|
||||
|
||||
// Also patch C++ side (GDevelop.js) watermark default
|
||||
const projectCpp = 'Core/GDCore/Project/Project.cpp';
|
||||
if (fs.existsSync(path.join(ROOT, projectCpp))) {
|
||||
let cpp = fs.readFileSync(path.join(ROOT, projectCpp), 'utf8');
|
||||
cpp = cpp.replace(/SetShowWatermark\(true\)/g, 'SetShowWatermark(false) /* BYOK */');
|
||||
cpp = cpp.replace(/showWatermark\s*=\s*true/g, 'showWatermark = false /* BYOK */');
|
||||
fs.writeFileSync(path.join(ROOT, projectCpp), cpp, 'utf8');
|
||||
log(' ✓ C++ watermark default → false');
|
||||
}
|
||||
|
||||
// ── 6. Write version stamp ─────────────────────────────────────────
|
||||
const stampPath = path.join(ROOT, '.gsd', 'BYOK_PATCHED');
|
||||
fs.mkdirSync(path.dirname(stampPath), { recursive: true });
|
||||
fs.writeFileSync(stampPath, JSON.stringify({
|
||||
version: RELEASE_TAG,
|
||||
patchedAt: new Date().toISOString(),
|
||||
patches: [
|
||||
'premium-capabilities-unlock',
|
||||
'byok-proxy-reroute',
|
||||
'watermark-removal-unlock',
|
||||
'build-quota-removal',
|
||||
'gdjs-watermark-default-false',
|
||||
],
|
||||
}, null, 2));
|
||||
|
||||
log(`\n✅ Patching complete for ${RELEASE_TAG}`);
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# setup.sh — One-time setup for the GDevelop BYOK project
|
||||
#
|
||||
# Usage: bash setup.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════╗"
|
||||
echo "║ GDevelop BYOK — Project Setup ║"
|
||||
echo "╚══════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# ── 1. Install proxy dependencies ────────────────────────────────────
|
||||
echo "[1/4] Installing AI proxy dependencies..."
|
||||
cd proxy
|
||||
npm install
|
||||
cd ..
|
||||
|
||||
# ── 2. Check for .env ────────────────────────────────────────────────
|
||||
if [ ! -f proxy/.env ]; then
|
||||
echo ""
|
||||
echo "[2/4] Creating proxy/.env from template..."
|
||||
cp proxy/.env.example proxy/.env
|
||||
echo " ⚠️ Edit proxy/.env with your API keys before using BYOK."
|
||||
else
|
||||
echo "[2/4] proxy/.env already exists — skipping."
|
||||
fi
|
||||
|
||||
# ── 3. GitHub Actions setup ──────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[3/4] GitHub Actions workflow is in .github/workflows/build-patched.yml"
|
||||
echo " The workflow will:"
|
||||
echo " 1. Check for new GDevelop releases every 30 minutes"
|
||||
echo " 2. Clone & patch the source"
|
||||
echo " 3. Build macOS, Linux, and Windows"
|
||||
echo " 4. Create a GitHub Release with artifacts"
|
||||
echo ""
|
||||
echo " To trigger manually:"
|
||||
echo " gh workflow run build-patched.yml -f release_tag=v5.6.269"
|
||||
|
||||
# ── 4. Quick start ──────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[4/4] Setup complete!"
|
||||
echo ""
|
||||
echo " ┌──────────────────────────────────────────────┐"
|
||||
echo " │ Quick start: │"
|
||||
echo " │ │"
|
||||
echo " │ 1. Edit proxy/.env with your API keys │"
|
||||
echo " │ 2. Start the proxy: cd proxy && npm start │"
|
||||
echo " │ 3. Run a patched build (see above) │"
|
||||
echo " │ │"
|
||||
echo " │ To build now from latest release: │"
|
||||
echo " │ bash scripts/build-local.sh │"
|
||||
echo " └──────────────────────────────────────────────┘"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user