From 5dd24f4c4558822deea101013407d0e7cd1ed6f3 Mon Sep 17 00:00:00 2001 From: ReedonMenu Date: Fri, 20 Feb 2026 13:24:02 +0000 Subject: [PATCH] Fix Copy Game CORS failure by using direct URL open/download --- README.md | 35 ++++- index.html | 269 +++++++++++++++++++++++++++++++++++++ roblox_game_copier.py | 97 +++++++++++++ script.js | 28 ++++ styles.css | 155 +++++++++++++++++++++ test_roblox_game_copier.py | 28 ++++ 6 files changed, 611 insertions(+), 1 deletion(-) create mode 100644 index.html create mode 100644 roblox_game_copier.py create mode 100644 script.js create mode 100644 styles.css create mode 100644 test_roblox_game_copier.py diff --git a/README.md b/README.md index 0b9a142..45138da 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,35 @@ # GameCopier -This is a roblox game copier please do not use the api key + +Dark-themed web UI + local CLI for duplicating **your own exported Roblox place files**. + +## Important +This project is for files you own and are authorized to copy. Do not use it to duplicate other creators' games without permission. + +## Web UI +Open `index.html` to use the themed “Game Copier” page. + +- Paste a local file path (`.rbxl` or `.rbxlx`) +- Click **COPY GAME!** +- The UI validates input and shows the prepared local-copy status + +## Python CLI +Generate key: + +```bash +python3 roblox_game_copier.py init-key +``` + +Copy local file: + +```bash +python3 roblox_game_copier.py copy \ + --api-key "" \ + --source ./MyGame.rbxl \ + --out-dir ./backups +``` + +## Tests + +```bash +python3 -m unittest -v +``` diff --git a/index.html b/index.html new file mode 100644 index 0000000..8590546 --- /dev/null +++ b/index.html @@ -0,0 +1,269 @@ + + + + + + Game Copier + + + + + + +
+ + +
+

Game Copier

+

Paste a game file URL (or local path for PowerShell) then click “Copy Game!”.

+ +
+ + + + +
Ready.
+
+ +
+

Join helper (friends with joins enabled only):

+ + + +
Paste a valid Roblox join URL. This does not bypass privacy settings.
+
+ +
+
    +
  • Use the Tools tab to download a file from a direct URL using your browser permission prompt.
  • +
  • Use “Copy as PowerShell” to generate a `.ps1` file for local `.rbxl/.rbxlx` backup copy actions.
  • +
  • Use Features tab to quickly open join links when your friend has joins enabled.
  • +
+
+ +
+
    +
  • This UI is local and does not clone protected games.
  • +
  • Joining only works when the target user allows joins.
  • +
  • No privacy/security bypass functionality is included.
  • +
+
+
+
+ + + + 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..a4549b5 --- /dev/null +++ b/script.js @@ -0,0 +1,28 @@ +const copyBtn = document.getElementById('copyBtn'); +const gameFileInput = document.getElementById('gameFile'); +const statusBox = document.getElementById('status'); + +function nowStamp() { + return new Date().toLocaleTimeString(); +} + +copyBtn.addEventListener('click', () => { + const file = gameFileInput.value.trim(); + + copyBtn.classList.remove('clicked'); + void copyBtn.offsetWidth; + copyBtn.classList.add('clicked'); + + if (!file) { + statusBox.textContent = `[${nowStamp()}] Enter a local .rbxl or .rbxlx file path first.`; + return; + } + + const isValid = file.endsWith('.rbxl') || file.endsWith('.rbxlx'); + if (!isValid) { + statusBox.textContent = `[${nowStamp()}] Unsupported file. Use .rbxl or .rbxlx.`; + return; + } + + statusBox.textContent = `[${nowStamp()}] Prepared local copy command for ${file} -> ./backups`; +}); diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..b013a31 --- /dev/null +++ b/styles.css @@ -0,0 +1,155 @@ +:root { + color-scheme: dark; + --bg: #0b0d11; + --bg-soft: #11141b; + --card: rgba(26, 28, 33, 0.82); + --card-border: rgba(123, 128, 144, 0.35); + --text: #f4f4f5; + --muted: #a1a1aa; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + font-family: 'Inter', system-ui, sans-serif; + color: var(--text); + background: + radial-gradient(70% 80% at 50% 55%, rgba(22, 25, 35, 0.85), transparent 70%), + linear-gradient(180deg, #0b0d11, #090b0f); +} + +.page { + min-height: 100vh; + display: grid; + place-items: center; + padding: 24px; +} + +.top-nav { + position: fixed; + top: 14px; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 10px; + padding: 10px 12px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(33, 33, 33, 0.64); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45), inset 0 0 18px rgba(255, 255, 255, 0.05); + backdrop-filter: blur(8px); +} + +.nav-link { + color: #a1a1aa; + text-decoration: none; + font-weight: 600; + padding: 10px 16px; + border-radius: 999px; +} + +.nav-link.active { + color: #f4f4f5; + background: radial-gradient(circle at 50% 0%, rgba(255, 255, 255, 0.28), rgba(255, 255, 255, 0.06)); +} + +.card { + width: min(540px, 95vw); + margin-top: 64px; + padding: 40px; + border-radius: 24px; + border: 1px solid var(--card-border); + background: linear-gradient(180deg, rgba(30, 31, 35, 0.88), rgba(22, 23, 27, 0.86)); + box-shadow: 0 25px 75px rgba(0, 0, 0, 0.58), inset 0 0 70px rgba(255, 255, 255, 0.02); +} + +h1 { + margin: 0; + text-align: center; + font-size: clamp(2rem, 4vw, 3rem); + text-shadow: 0 0 16px rgba(255, 255, 255, 0.2); +} + +.subtext { + margin: 16px 0 22px; + text-align: center; + color: var(--muted); + line-height: 1.45; + font-size: 1.05rem; +} + +input { + width: 100%; + border-radius: 16px; + border: 2px solid rgba(126, 129, 140, 0.32); + background: rgba(8, 9, 12, 0.95); + color: #e4e4e7; + padding: 16px 18px; + font-size: 1.2rem; + outline: none; +} + +input:focus { + border-color: rgba(210, 210, 220, 0.8); + box-shadow: 0 0 0 4px rgba(211, 211, 220, 0.12); +} + +.copy-btn { + width: 100%; + margin-top: 16px; + border: 0; + border-radius: 16px; + background: linear-gradient(180deg, #f4f4f5, #d4d4d8); + color: #111111; + font-size: 2rem; + font-weight: 800; + letter-spacing: 0.04em; + padding: 18px 14px; + cursor: pointer; + transition: transform 130ms ease, box-shadow 130ms ease; + box-shadow: 0 0 24px rgba(255, 255, 255, 0.25); +} + +.copy-btn:hover { + transform: translateY(-1px); +} + +.copy-btn:active { + transform: translateY(1px) scale(0.99); +} + +.copy-btn.clicked { + animation: pulse 450ms ease; +} + +@keyframes pulse { + 0% { box-shadow: 0 0 24px rgba(255, 255, 255, 0.3); } + 50% { box-shadow: 0 0 42px rgba(255, 255, 255, 0.7); } + 100% { box-shadow: 0 0 24px rgba(255, 255, 255, 0.3); } +} + +.status { + margin-top: 14px; + border-radius: 12px; + border: 1px solid rgba(112, 116, 130, 0.35); + background: rgba(10, 11, 16, 0.92); + color: #a1a1aa; + padding: 14px; + min-height: 56px; + font-size: 0.95rem; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} 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()