diff --git a/README.md b/README.md index 0b9a142..74d3c6d 100644 --- a/README.md +++ b/README.md @@ -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 "" \ + --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 +``` diff --git a/index.html b/index.html new file mode 100644 index 0000000..37e1486 --- /dev/null +++ b/index.html @@ -0,0 +1,57 @@ + + + + + + GameCopier Control Center + + + + + + + + +
+
+
+

GAMECOPIER

+

Advanced Backup UI

+

Generate an API key and prepare protected Roblox backups with a premium animated interface.

+
+ +
+ + + + + +
+ +
+ + +
+ +
[8:08:13 PM] Prepared backup command for ./SpaceAdventure.rbxl → ./backups
+
+
+ + + + diff --git a/roblox_game_copier.py b/roblox_game_copier.py new file mode 100644 index 0000000..e266b53 --- /dev/null +++ b/roblox_game_copier.py @@ -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()) diff --git a/script.js b/script.js new file mode 100644 index 0000000..7bc1d95 --- /dev/null +++ b/script.js @@ -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(); + }); +})(); diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..f5b7f9a --- /dev/null +++ b/styles.css @@ -0,0 +1,202 @@ +:root { + color-scheme: dark; + --bg-a: #020824; + --bg-b: #071244; + --panel: rgba(7, 16, 60, 0.82); + --panel-border: rgba(81, 114, 220, 0.45); + --field: rgba(8, 15, 53, 0.95); + --field-border: rgba(73, 102, 203, 0.58); + --text: #d8e3ff; + --muted: #8fa0d6; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + font-family: 'Inter', system-ui, sans-serif; + color: var(--text); + background: + radial-gradient(45vw 45vh at 80% 18%, rgba(141, 58, 219, 0.55), transparent 72%), + radial-gradient(70vw 50vh at 37% 42%, rgba(20, 43, 128, 0.35), transparent 80%), + linear-gradient(115deg, #1b275d 0%, #050d2d 34%, #09174e 100%); + overflow: hidden; +} + +#starfield { + position: fixed; + inset: 0; + z-index: 0; +} + +.app-shell { + position: relative; + z-index: 1; + min-height: 100vh; + display: grid; + place-items: center; + padding: 32px; +} + +.panel { + width: min(860px, 95vw); + border-radius: 26px; + border: 1px solid var(--panel-border); + background: var(--panel); + box-shadow: 0 0 0 1px rgba(31, 55, 140, 0.2) inset, 0 28px 70px rgba(2, 6, 28, 0.75); + padding: 32px; +} + +.eyebrow { + margin: 0; + font-size: 0.8rem; + letter-spacing: 0.18em; + color: #8ea5de; +} + +h1 { + margin: 10px 0 6px; + font-size: clamp(2rem, 4.2vw, 3.2rem); + line-height: 1.03; +} + +.subtitle { + margin: 0 0 20px; + color: var(--muted); + font-size: 1.05rem; +} + +.grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; +} + +label { + display: grid; + gap: 8px; +} + +label span { + font-size: 1.06rem; + font-weight: 700; + color: #c0cef4; +} + +input { + width: 100%; + border-radius: 14px; + border: 1px solid var(--field-border); + background: var(--field); + color: #c8d5ff; + padding: 12px 16px; + font-size: 1.05rem; + outline: none; +} + +input:focus { + border-color: #7cb8ff; + box-shadow: 0 0 0 3px rgba(118, 184, 255, 0.22); +} + +.actions { + margin-top: 18px; + display: flex; + gap: 12px; +} + +.btn { + position: relative; + border: 1px solid transparent; + border-radius: 999px; + padding: 12px 22px; + font-size: 1.75rem; + font-weight: 800; + line-height: 1; + cursor: pointer; + overflow: hidden; + transition: transform 170ms ease, filter 170ms ease; +} + +.btn:hover { + transform: translateY(-1px); +} + +.btn:active { + transform: translateY(1px) scale(0.98); +} + +.btn-secondary { + color: #e8efff; + background: linear-gradient(180deg, #3f4f96, #2f3d7a); + border-color: rgba(170, 190, 255, 0.38); +} + +.btn-primary { + color: #002122; + background: linear-gradient(90deg, #58ffc3, #71f8db); + box-shadow: 0 10px 24px rgba(86, 249, 206, 0.38); +} + +.btn-shine { + position: absolute; + top: -150%; + left: -40%; + width: 45%; + height: 300%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.62), transparent); + transform: rotate(20deg); + animation: shine 2.4s linear infinite; + pointer-events: none; +} + +@keyframes shine { + from { + left: -45%; + } + to { + left: 125%; + } +} + +.ripple { + position: absolute; + width: 12px; + height: 12px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.75); + transform: translate(-50%, -50%) scale(0); + animation: ripple 650ms ease-out forwards; + pointer-events: none; +} + +@keyframes ripple { + to { + transform: translate(-50%, -50%) scale(20); + opacity: 0; + } +} + +.status { + margin-top: 16px; + border-radius: 14px; + border: 1px solid var(--field-border); + background: rgba(2, 9, 36, 0.95); + color: #9fb3ec; + padding: 20px; + min-height: 76px; + font-size: 1.05rem; +} + +@media (max-width: 900px) { + .grid { + grid-template-columns: 1fr; + } + + .btn { + font-size: 1.05rem; + } +} diff --git a/test_roblox_game_copier.py b/test_roblox_game_copier.py new file mode 100644 index 0000000..7f8212b --- /dev/null +++ b/test_roblox_game_copier.py @@ -0,0 +1,28 @@ +import tempfile +import unittest +from pathlib import Path + +import roblox_game_copier as gc + + +class GameCopierTests(unittest.TestCase): + def test_generate_and_validate_key(self): + with tempfile.TemporaryDirectory() as tmp: + key_file = Path(tmp) / '.key' + key = gc.generate_api_key() + gc.save_key(key, key_file) + self.assertTrue(gc.validate_api_key(key, key_file)) + self.assertFalse(gc.validate_api_key('bad-key', key_file)) + + def test_copy_game_file(self): + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / 'mygame.rbxl' + source.write_text('dummy-content', encoding='utf-8') + out_dir = Path(tmp) / 'backups' + copied = gc.copy_game_file(source, out_dir) + self.assertTrue(copied.exists()) + self.assertEqual(copied.suffix, '.rbxl') + + +if __name__ == '__main__': + unittest.main()