Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,38 @@
# GameCopier
This is a roblox game copier please do not use the api key

A Roblox game backup toolkit with:
- a Python CLI for protected local backups
- an advanced animated HTML control-center UI

> Use this only for place files you own and exported locally (`.rbxl` / `.rbxlx`).

## Python CLI quick start

1. Generate your API key:

```bash
python3 roblox_game_copier.py init-key
```

2. Copy your own place file into a backup folder:

```bash
python3 roblox_game_copier.py copy \
--api-key "<PASTE_THE_KEY_YOU_GENERATED>" \
--source ./MyGame.rbxl \
--out-dir ./backups
```

## Advanced UI quick start

Open `index.html` in your browser for a modern control center with:
- glowing glassmorphism layout
- animated starfield background
- ripple click animation on buttons
- live status and simulated backup action flow

## Run tests

```bash
python3 -m unittest -v
```
57 changes: 57 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GameCopier Control Center</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<canvas id="starfield" aria-hidden="true"></canvas>

<main class="app-shell">
<section class="panel">
<header>
<p class="eyebrow">GAMECOPIER</p>
<h1>Advanced Backup UI</h1>
<p class="subtitle">Generate an API key and prepare protected Roblox backups with a premium animated interface.</p>
</header>

<div class="grid">
<label>
<span>API Key</span>
<input id="apiKey" type="text" value="GC-4e8Nn1zWOahmyof3MwjwAgS6Sy3uC_" />
</label>

<label>
<span>Source Game File</span>
<input id="source" type="text" value="./SpaceAdventure.rbxl" />
</label>

<label>
<span>Output Folder</span>
<input id="outDir" type="text" value="./backups" />
</label>
</div>

<div class="actions">
<button id="generateKey" class="btn btn-secondary">Generate Key</button>
<button id="copyGame" class="btn btn-primary">
<span class="btn-text">Backup Queued</span>
<span class="btn-shine" aria-hidden="true"></span>
</button>
</div>

<pre id="status" class="status">[8:08:13 PM] Prepared backup command for ./SpaceAdventure.rbxl → ./backups</pre>
</section>
</main>

<script src="script.js"></script>
</body>
</html>
97 changes: 97 additions & 0 deletions roblox_game_copier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Local Roblox game backup copier protected by a custom API key.

This tool is intentionally for copying Roblox place files you already own and have
exported locally (.rbxl or .rbxlx).
"""

from __future__ import annotations

import argparse
import hashlib
import secrets
import shutil
import sys
from datetime import datetime, timezone
from pathlib import Path

DEFAULT_KEY_FILE = Path('.gamecopier_api_key')


def generate_api_key(prefix: str = 'GC') -> str:
return f"{prefix}-{secrets.token_urlsafe(32)}"


def hash_key(raw_key: str) -> str:
return hashlib.sha256(raw_key.encode('utf-8')).hexdigest()


def save_key(raw_key: str, key_file: Path = DEFAULT_KEY_FILE) -> None:
key_file.write_text(hash_key(raw_key), encoding='utf-8')


def load_key_hash(key_file: Path = DEFAULT_KEY_FILE) -> str:
if not key_file.exists():
raise FileNotFoundError(
f"API key file '{key_file}' was not found. Run init-key first."
)
return key_file.read_text(encoding='utf-8').strip()


def validate_api_key(raw_key: str, key_file: Path = DEFAULT_KEY_FILE) -> bool:
return hash_key(raw_key) == load_key_hash(key_file)


def copy_game_file(source: Path, destination_dir: Path) -> Path:
if source.suffix.lower() not in {'.rbxl', '.rbxlx'}:
raise ValueError('Source must be a .rbxl or .rbxlx file.')
if not source.exists():
raise FileNotFoundError(f"Source file '{source}' does not exist.")

destination_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
destination_file = destination_dir / f"{source.stem}-backup-{timestamp}{source.suffix}"
shutil.copy2(source, destination_file)
return destination_file


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description='Copy your own exported Roblox game file with API-key protection.'
)
subparsers = parser.add_subparsers(dest='command', required=True)

subparsers.add_parser('init-key', help='Generate and save a new API key.')

copy_parser = subparsers.add_parser('copy', help='Copy a local .rbxl/.rbxlx file.')
copy_parser.add_argument('--api-key', required=True, help='Your custom GameCopier API key.')
copy_parser.add_argument('--source', required=True, help='Path to your source .rbxl/.rbxlx file.')
copy_parser.add_argument('--out-dir', default='backups', help='Backup destination directory.')

return parser.parse_args()


def main() -> int:
args = parse_args()

if args.command == 'init-key':
key = generate_api_key()
save_key(key)
print('Generated new GameCopier API key (save this now, it is not stored in plaintext):')
print(key)
return 0

if args.command == 'copy':
if not validate_api_key(args.api_key):
print('Invalid API key.', file=sys.stderr)
return 1

copied_to = copy_game_file(Path(args.source), Path(args.out_dir))
print(f'Backup created: {copied_to}')
return 0

return 1


if __name__ == '__main__':
raise SystemExit(main())
110 changes: 110 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
const statusBox = document.getElementById('status');
const apiKeyInput = document.getElementById('apiKey');
const sourceInput = document.getElementById('source');
const outDirInput = document.getElementById('outDir');
const generateKeyBtn = document.getElementById('generateKey');
const copyBtn = document.getElementById('copyGame');

function log(message) {
const stamp = new Date().toLocaleTimeString();
statusBox.textContent = `[${stamp}] ${message}`;
}

function makeApiKey() {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
const randomPart = Array.from(crypto.getRandomValues(new Uint8Array(36)))
.map(n => alphabet[n % alphabet.length])
.join('');
return `GC-${randomPart}`;
}

function spawnRipple(button, event) {
const circle = document.createElement('span');
circle.className = 'ripple';
const rect = button.getBoundingClientRect();
circle.style.left = `${event.clientX - rect.left}px`;
circle.style.top = `${event.clientY - rect.top}px`;
button.appendChild(circle);
circle.addEventListener('animationend', () => circle.remove());
}

generateKeyBtn.addEventListener('click', event => {
spawnRipple(generateKeyBtn, event);
const key = makeApiKey();
apiKeyInput.value = key;
apiKeyInput.select();
log('Generated a new API key. Copy and use it with the Python CLI.');
});

copyBtn.addEventListener('click', async event => {
spawnRipple(copyBtn, event);

if (!apiKeyInput.value.trim()) {
log('Please generate or paste an API key first.');
return;
}

if (!sourceInput.value.trim()) {
log('Please provide a source .rbxl or .rbxlx path.');
return;
}

copyBtn.classList.add('success');
copyBtn.querySelector('.btn-text').textContent = 'Backup Queued';
log(`Prepared backup command for ${sourceInput.value} → ${outDirInput.value || './backups'}`);

await new Promise(resolve => setTimeout(resolve, 1150));

copyBtn.classList.remove('success');
copyBtn.querySelector('.btn-text').textContent = 'Create Backup';
});

(function animateStarfield() {
const canvas = document.getElementById('starfield');
const ctx = canvas.getContext('2d');
const stars = [];

function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}

function init() {
stars.length = 0;
const count = Math.floor((window.innerWidth * window.innerHeight) / 10000);
for (let i = 0; i < count; i += 1) {
stars.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
r: Math.random() * 1.6,
v: 0.08 + Math.random() * 0.28,
a: 0.3 + Math.random() * 0.7,
});
}
}

function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
stars.forEach(star => {
star.y += star.v;
if (star.y > canvas.height) {
star.y = -2;
star.x = Math.random() * canvas.width;
}
ctx.beginPath();
ctx.fillStyle = `rgba(180,210,255,${star.a})`;
ctx.arc(star.x, star.y, star.r, 0, Math.PI * 2);
ctx.fill();
});
requestAnimationFrame(draw);
}

resize();
init();
draw();

window.addEventListener('resize', () => {
resize();
init();
});
})();
Loading