From 713c9d20dc693be370e32790f3b2a89be99c12f7 Mon Sep 17 00:00:00 2001 From: ReedonMenu Date: Thu, 19 Feb 2026 20:03:03 +0000 Subject: [PATCH 1/3] Add API-key-protected local Roblox backup copier --- README.md | 28 ++++++++++- roblox_game_copier.py | 97 ++++++++++++++++++++++++++++++++++++++ test_roblox_game_copier.py | 28 +++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 roblox_game_copier.py create mode 100644 test_roblox_game_copier.py diff --git a/README.md b/README.md index 0b9a142..7a96942 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,28 @@ # GameCopier -This is a roblox game copier please do not use the api key + +A **local Roblox game backup copier** protected by a custom API key. + +> This project is for backing up Roblox game files that you already own and exported locally (`.rbxl` / `.rbxlx`). + +## 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 +``` + +## Run tests + +```bash +python3 -m unittest -v +``` 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/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() From b7176b6bd1e06cc85dd474eda5bc09c029d09bfe Mon Sep 17 00:00:00 2001 From: ReedonMenu Date: Thu, 19 Feb 2026 20:09:31 +0000 Subject: [PATCH 2/3] Add advanced animated HTML control center UI --- README.md | 16 ++++- index.html | 56 +++++++++++++++++ script.js | 110 ++++++++++++++++++++++++++++++++ styles.css | 179 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 358 insertions(+), 3 deletions(-) create mode 100644 index.html create mode 100644 script.js create mode 100644 styles.css diff --git a/README.md b/README.md index 7a96942..74d3c6d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # GameCopier -A **local Roblox game backup copier** protected by a custom API key. +A Roblox game backup toolkit with: +- a Python CLI for protected local backups +- an advanced animated HTML control-center UI -> This project is for backing up Roblox game files that you already own and exported locally (`.rbxl` / `.rbxlx`). +> Use this only for place files you own and exported locally (`.rbxl` / `.rbxlx`). -## Quick start +## Python CLI quick start 1. Generate your API key: @@ -21,6 +23,14 @@ python3 roblox_game_copier.py copy \ --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 diff --git a/index.html b/index.html new file mode 100644 index 0000000..700b585 --- /dev/null +++ b/index.html @@ -0,0 +1,56 @@ + + + + + + GameCopier Control Center + + + + + + + +
+
+
+

GameCopier

+

Advanced Backup UI

+

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

+
+ +
+ + + + + +
+ +
+ + +
+ +
Ready.
+
+
+ + + + 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..f3d5efe --- /dev/null +++ b/styles.css @@ -0,0 +1,179 @@ +:root { + color-scheme: dark; + --bg-1: #050816; + --bg-2: #0b1330; + --panel: rgba(12, 18, 47, 0.65); + --border: rgba(140, 161, 255, 0.3); + --text: #eaf0ff; + --muted: #9fb0dc; + --primary-a: #6a7cff; + --primary-b: #63dbff; + --success: #53f6c2; +} + +* { box-sizing: border-box; } +body { + margin: 0; + min-height: 100vh; + font-family: 'Inter', system-ui, sans-serif; + background: radial-gradient(circle at 10% 10%, #1a2a64, transparent 40%), + radial-gradient(circle at 90% 20%, #541a75, transparent 35%), + linear-gradient(145deg, var(--bg-1), var(--bg-2)); + color: var(--text); + overflow: hidden; +} + +#starfield { + position: fixed; + inset: 0; + z-index: 0; +} + +.app-shell { + position: relative; + z-index: 1; + display: grid; + place-items: center; + min-height: 100vh; + padding: 1.25rem; +} + +.panel { + width: min(860px, 95vw); + backdrop-filter: blur(20px); + background: var(--panel); + border: 1px solid var(--border); + border-radius: 24px; + padding: 2rem; + box-shadow: 0 25px 80px rgba(11, 15, 40, 0.65); +} + +.glow { + animation: float 5s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-6px); } +} + +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.13em; + font-size: 0.78rem; + color: var(--muted); + margin: 0; +} + +h1 { margin: 0.35rem 0; font-size: clamp(1.8rem, 4vw, 2.8rem); } +.subtitle { color: var(--muted); margin: 0 0 1.5rem; } + +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1rem; +} + +label { + display: grid; + gap: 0.5rem; + font-weight: 600; + color: #dce6ff; +} + +input { + border: 1px solid rgba(148, 173, 255, 0.25); + background: rgba(12, 19, 49, 0.75); + color: var(--text); + border-radius: 12px; + padding: 0.85rem 0.95rem; + transition: all 200ms ease; +} + +input:focus { + outline: none; + border-color: #84f0ff; + box-shadow: 0 0 0 4px rgba(119, 228, 255, 0.2); +} + +.actions { + margin-top: 1.25rem; + display: flex; + gap: 0.85rem; + flex-wrap: wrap; +} + +.btn { + position: relative; + overflow: hidden; + border: 0; + border-radius: 999px; + padding: 0.85rem 1.25rem; + font-weight: 700; + cursor: pointer; + transform: translateY(0) scale(1); + transition: transform 170ms ease, filter 170ms ease; +} + +.btn:hover { transform: translateY(-2px) scale(1.01); } +.btn:active { transform: translateY(1px) scale(0.98); } + +.btn-secondary { + background: rgba(153, 172, 255, 0.2); + color: #eff4ff; + border: 1px solid rgba(173, 193, 255, 0.4); +} + +.btn-primary { + background: linear-gradient(110deg, var(--primary-a), var(--primary-b)); + color: #031322; + box-shadow: 0 15px 30px rgba(93, 178, 255, 0.35); +} + +.btn-primary.success { + background: linear-gradient(110deg, var(--success), #95ffd6); +} + +.btn-shine { + position: absolute; + inset: -150% auto auto -30%; + width: 55%; + height: 300%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.65), transparent); + transform: rotate(22deg); + animation: shine 2.3s linear infinite; + pointer-events: none; +} + +@keyframes shine { + from { left: -50%; } + to { left: 130%; } +} + +.ripple { + position: absolute; + width: 14px; + height: 14px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.6); + transform: translate(-50%, -50%) scale(0); + animation: ripple 650ms ease-out forwards; + pointer-events: none; +} + +@keyframes ripple { + to { + transform: translate(-50%, -50%) scale(18); + opacity: 0; + } +} + +.status { + margin-top: 1rem; + padding: 1rem; + border-radius: 14px; + border: 1px solid rgba(142, 163, 255, 0.3); + background: rgba(4, 9, 28, 0.7); + color: #c5d3fa; + min-height: 76px; +} From d906427d22f72906920e919bd002e0427bd2fea9 Mon Sep 17 00:00:00 2001 From: ReedonMenu Date: Thu, 19 Feb 2026 20:14:47 +0000 Subject: [PATCH 3/3] Restyle UI to closely match provided reference theme --- index.html | 15 +++-- styles.css | 193 ++++++++++++++++++++++++++++++----------------------- 2 files changed, 116 insertions(+), 92 deletions(-) diff --git a/index.html b/index.html index 700b585..37e1486 100644 --- a/index.html +++ b/index.html @@ -14,10 +14,11 @@ +
-
+
-

GameCopier

+

GAMECOPIER

Advanced Backup UI

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

@@ -25,29 +26,29 @@

Advanced Backup UI

-
Ready.
+
[8:08:13 PM] Prepared backup command for ./SpaceAdventure.rbxl → ./backups
diff --git a/styles.css b/styles.css index f3d5efe..f5b7f9a 100644 --- a/styles.css +++ b/styles.css @@ -1,25 +1,28 @@ :root { color-scheme: dark; - --bg-1: #050816; - --bg-2: #0b1330; - --panel: rgba(12, 18, 47, 0.65); - --border: rgba(140, 161, 255, 0.3); - --text: #eaf0ff; - --muted: #9fb0dc; - --primary-a: #6a7cff; - --primary-b: #63dbff; - --success: #53f6c2; -} - -* { box-sizing: border-box; } + --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; - background: radial-gradient(circle at 10% 10%, #1a2a64, transparent 40%), - radial-gradient(circle at 90% 20%, #541a75, transparent 35%), - linear-gradient(145deg, var(--bg-1), var(--bg-2)); 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; } @@ -32,130 +35,139 @@ body { .app-shell { position: relative; z-index: 1; + min-height: 100vh; display: grid; place-items: center; - min-height: 100vh; - padding: 1.25rem; + padding: 32px; } .panel { width: min(860px, 95vw); - backdrop-filter: blur(20px); + border-radius: 26px; + border: 1px solid var(--panel-border); background: var(--panel); - border: 1px solid var(--border); - border-radius: 24px; - padding: 2rem; - box-shadow: 0 25px 80px rgba(11, 15, 40, 0.65); + box-shadow: 0 0 0 1px rgba(31, 55, 140, 0.2) inset, 0 28px 70px rgba(2, 6, 28, 0.75); + padding: 32px; } -.glow { - animation: float 5s ease-in-out infinite; +.eyebrow { + margin: 0; + font-size: 0.8rem; + letter-spacing: 0.18em; + color: #8ea5de; } -@keyframes float { - 0%, 100% { transform: translateY(0px); } - 50% { transform: translateY(-6px); } +h1 { + margin: 10px 0 6px; + font-size: clamp(2rem, 4.2vw, 3.2rem); + line-height: 1.03; } -.eyebrow { - text-transform: uppercase; - letter-spacing: 0.13em; - font-size: 0.78rem; +.subtitle { + margin: 0 0 20px; color: var(--muted); - margin: 0; + font-size: 1.05rem; } -h1 { margin: 0.35rem 0; font-size: clamp(1.8rem, 4vw, 2.8rem); } -.subtitle { color: var(--muted); margin: 0 0 1.5rem; } - .grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 1rem; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; } label { display: grid; - gap: 0.5rem; - font-weight: 600; - color: #dce6ff; + gap: 8px; +} + +label span { + font-size: 1.06rem; + font-weight: 700; + color: #c0cef4; } input { - border: 1px solid rgba(148, 173, 255, 0.25); - background: rgba(12, 19, 49, 0.75); - color: var(--text); - border-radius: 12px; - padding: 0.85rem 0.95rem; - transition: all 200ms ease; + 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 { - outline: none; - border-color: #84f0ff; - box-shadow: 0 0 0 4px rgba(119, 228, 255, 0.2); + border-color: #7cb8ff; + box-shadow: 0 0 0 3px rgba(118, 184, 255, 0.22); } .actions { - margin-top: 1.25rem; + margin-top: 18px; display: flex; - gap: 0.85rem; - flex-wrap: wrap; + gap: 12px; } .btn { position: relative; - overflow: hidden; - border: 0; + border: 1px solid transparent; border-radius: 999px; - padding: 0.85rem 1.25rem; - font-weight: 700; + padding: 12px 22px; + font-size: 1.75rem; + font-weight: 800; + line-height: 1; cursor: pointer; - transform: translateY(0) scale(1); + overflow: hidden; transition: transform 170ms ease, filter 170ms ease; } -.btn:hover { transform: translateY(-2px) scale(1.01); } -.btn:active { transform: translateY(1px) scale(0.98); } +.btn:hover { + transform: translateY(-1px); +} -.btn-secondary { - background: rgba(153, 172, 255, 0.2); - color: #eff4ff; - border: 1px solid rgba(173, 193, 255, 0.4); +.btn:active { + transform: translateY(1px) scale(0.98); } -.btn-primary { - background: linear-gradient(110deg, var(--primary-a), var(--primary-b)); - color: #031322; - box-shadow: 0 15px 30px rgba(93, 178, 255, 0.35); +.btn-secondary { + color: #e8efff; + background: linear-gradient(180deg, #3f4f96, #2f3d7a); + border-color: rgba(170, 190, 255, 0.38); } -.btn-primary.success { - background: linear-gradient(110deg, var(--success), #95ffd6); +.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; - inset: -150% auto auto -30%; - width: 55%; + top: -150%; + left: -40%; + width: 45%; height: 300%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.65), transparent); - transform: rotate(22deg); - animation: shine 2.3s linear infinite; + 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: -50%; } - to { left: 130%; } + from { + left: -45%; + } + to { + left: 125%; + } } .ripple { position: absolute; - width: 14px; - height: 14px; + width: 12px; + height: 12px; border-radius: 50%; - background: rgba(255, 255, 255, 0.6); + background: rgba(255, 255, 255, 0.75); transform: translate(-50%, -50%) scale(0); animation: ripple 650ms ease-out forwards; pointer-events: none; @@ -163,17 +175,28 @@ input:focus { @keyframes ripple { to { - transform: translate(-50%, -50%) scale(18); + transform: translate(-50%, -50%) scale(20); opacity: 0; } } .status { - margin-top: 1rem; - padding: 1rem; + margin-top: 16px; border-radius: 14px; - border: 1px solid rgba(142, 163, 255, 0.3); - background: rgba(4, 9, 28, 0.7); - color: #c5d3fa; + 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; + } }