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
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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 "<PASTE_KEY>" \
--source ./MyGame.rbxl \
--out-dir ./backups
```

## Tests

```bash
python3 -m unittest -v
```
269 changes: 269 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Game Copier</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;700;800&display=swap" rel="stylesheet" />
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: 'Inter', system-ui, sans-serif;
color: #f4f4f5;
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; border: 0; background: transparent; cursor: pointer;
}
.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(620px, 95vw); margin-top: 64px; padding: 40px; border-radius: 24px;
border: 1px solid rgba(123,128,144,0.35);
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,.2); }
.subtext { margin: 16px 0 22px; text-align: center; color: #a1a1aa; line-height: 1.45; font-size: 1.02rem; }
.panel { display: none; }
.panel.active { display: block; }
input {
width: 100%; border-radius: 16px; border: 2px solid rgba(126,129,140,.32); background: rgba(8,9,12,.95);
color: #e4e4e7; padding: 14px 16px; font-size: 1rem; outline: none; margin-bottom: 12px;
}
input:focus { border-color: rgba(210,210,220,.8); box-shadow: 0 0 0 4px rgba(211,211,220,.12); }
.btn {
width: 100%; border: 0; border-radius: 16px; background: linear-gradient(180deg, #f4f4f5, #d4d4d8);
color: #111; font-size: 1.2rem; font-weight: 800; letter-spacing: .04em;
padding: 14px; cursor: pointer; transition: transform 130ms ease, box-shadow 130ms ease;
box-shadow: 0 0 24px rgba(255,255,255,.18);
}
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(1px) scale(.99); }
.btn.clicked { animation: pulse 420ms ease; }
.btn.secondary { margin-top: 8px; background: linear-gradient(180deg, #373a45, #2d3038); color: #ececf0; font-size: .95rem; }
@keyframes pulse { 0%{box-shadow:0 0 24px rgba(255,255,255,.2)} 50%{box-shadow:0 0 36px rgba(255,255,255,.45)} 100%{box-shadow:0 0 24px rgba(255,255,255,.2)} }
.status {
margin-top: 12px; border-radius: 12px; border: 1px solid rgba(112,116,130,.35);
background: rgba(10,11,16,.92); color: #a1a1aa; padding: 14px; min-height: 56px; font-size: .93rem;
}
ul { margin: 0; padding-left: 18px; color: #b7b7be; line-height: 1.5; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
</style>
</head>
<body>
<main class="page">
<nav class="top-nav" aria-label="Primary">
<button class="nav-link" data-tab="home">Home</button>
<button class="nav-link" data-tab="features">Features</button>
<button class="nav-link" data-tab="info">Info</button>
<button class="nav-link active" data-tab="tools">Tools</button>
</nav>

<section class="card" aria-labelledby="title">
<h1 id="title">Game Copier</h1>
<p class="subtext" id="subtitle">Paste a game file URL (or local path for PowerShell) then click “Copy Game!”.</p>

<div id="panel-tools" class="panel active">
<label for="gameFile" class="sr-only">Enter game file</label>
<input id="gameFile" type="text" placeholder="Enter URL (https://...) or path (C:\\Games\\MyGame.rbxl)" />
<button id="copyBtn" class="btn">COPY GAME!</button>
<button id="psDownloadBtn" class="btn secondary">COPY AS POWERSHELL (.ps1)</button>
<pre id="status" class="status">Ready.</pre>
</div>

<div id="panel-features" class="panel">
<p class="subtext" style="margin-top:0">Join helper (friends with joins enabled only):</p>
<input id="joinUrl" type="text" placeholder="Paste Roblox join link from friend/profile" />
<button id="joinBtn" class="btn">JOIN FRIEND</button>
<button id="friendsPageBtn" class="btn secondary">Open Roblox Friends Page</button>
<pre id="joinStatus" class="status">Paste a valid Roblox join URL. This does not bypass privacy settings.</pre>
</div>

<div id="panel-home" class="panel">
<ul>
<li>Use the Tools tab to download a file from a direct URL using your browser permission prompt.</li>
<li>Use “Copy as PowerShell” to generate a `.ps1` file for local `.rbxl/.rbxlx` backup copy actions.</li>
<li>Use Features tab to quickly open join links when your friend has joins enabled.</li>
</ul>
</div>

<div id="panel-info" class="panel">
<ul>
<li>This UI is local and does not clone protected games.</li>
<li>Joining only works when the target user allows joins.</li>
<li>No privacy/security bypass functionality is included.</li>
</ul>
</div>
</section>
</main>

<script>
const navButtons = document.querySelectorAll('.nav-link');
const panels = {
tools: document.getElementById('panel-tools'),
features: document.getElementById('panel-features'),
home: document.getElementById('panel-home'),
info: document.getElementById('panel-info'),
};

function activateTab(name) {
navButtons.forEach(btn => btn.classList.toggle('active', btn.dataset.tab === name));
Object.entries(panels).forEach(([key, panel]) => panel.classList.toggle('active', key === name));
}

navButtons.forEach(btn => btn.addEventListener('click', () => activateTab(btn.dataset.tab)));

const copyBtn = document.getElementById('copyBtn');
const psDownloadBtn = document.getElementById('psDownloadBtn');
const gameFileInput = document.getElementById('gameFile');
const statusBox = document.getElementById('status');

function stamp() { return new Date().toLocaleTimeString(); }
function clickAnim(button) {
button.classList.remove('clicked');
void button.offsetWidth;
button.classList.add('clicked');
}

function isGameFileExtension(value) {
const lower = value.toLowerCase();
return lower.endsWith('.rbxl') || lower.endsWith('.rbxlx');
}

function validateLocalGamePath(file) {
if (!file) {
statusBox.textContent = `[${stamp()}] Enter a local .rbxl or .rbxlx file path first.`;
return false;
}
if (!isGameFileExtension(file)) {
statusBox.textContent = `[${stamp()}] Unsupported file. Use .rbxl or .rbxlx.`;
return false;
}
return true;
}

function triggerTextDownload(name, content) {
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = blobUrl;
link.download = name;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(blobUrl);
}

copyBtn.addEventListener('click', () => {
clickAnim(copyBtn);
const value = gameFileInput.value.trim();
if (!value) {
statusBox.textContent = `[${stamp()}] Enter a direct download URL first.`;
return;
}

const isHttpUrl = /^https?:\/\//i.test(value);
if (!isHttpUrl) {
statusBox.textContent = `[${stamp()}] Local paths can't be downloaded by browsers directly. Use COPY AS POWERSHELL.`;
return;
}

const allowed = window.confirm('Allow this website to open/download this URL?');
if (!allowed) {
statusBox.textContent = `[${stamp()}] Download cancelled. Permission was not granted.`;
return;
}

const parsedUrl = new URL(value);
const fileNameFromUrl = parsedUrl.pathname.split('/').pop() || 'downloaded-file';

const link = document.createElement('a');
link.href = value;
link.target = '_blank';
link.rel = 'noopener noreferrer';
if (fileNameFromUrl.includes('.')) {
link.download = fileNameFromUrl;
}

document.body.appendChild(link);
link.click();
link.remove();

statusBox.textContent = `[${stamp()}] Opened URL in a new tab. If it's a direct file link, your browser will download it.`;
});

psDownloadBtn.addEventListener('click', () => {
clickAnim(psDownloadBtn);
const file = gameFileInput.value.trim();
if (!validateLocalGamePath(file)) return;

const allowed = window.confirm('Allow this website to download a PowerShell script (.ps1) to your computer?');
if (!allowed) {
statusBox.textContent = `[${stamp()}] Download cancelled. Permission was not granted.`;
return;
}

const escapedPath = file.replace(/'/g, "''");
const script = [
'$ErrorActionPreference = "Stop"',
`$source = '${escapedPath}'`,
'$destinationDir = Join-Path $PSScriptRoot "backups"',
'if (-not (Test-Path $source)) { throw "Source file not found: $source" }',
'if (-not (Test-Path $destinationDir)) { New-Item -ItemType Directory -Path $destinationDir | Out-Null }',
'$timestamp = Get-Date -Format "yyyyMMddTHHmmssZ"',
'$name = [System.IO.Path]::GetFileNameWithoutExtension($source)',
'$ext = [System.IO.Path]::GetExtension($source)',
'$destination = Join-Path $destinationDir ("{0}-backup-{1}{2}" -f $name, $timestamp, $ext)',
'Copy-Item -Path $source -Destination $destination -Force',
'Write-Host "Backup created: $destination"'
].join('\r\n');

triggerTextDownload('game-copy-backup.ps1', script);
statusBox.textContent = `[${stamp()}] Downloaded PowerShell script: game-copy-backup.ps1`;
});

const joinBtn = document.getElementById('joinBtn');
const friendsPageBtn = document.getElementById('friendsPageBtn');
const joinUrl = document.getElementById('joinUrl');
const joinStatus = document.getElementById('joinStatus');

joinBtn.addEventListener('click', () => {
clickAnim(joinBtn);
const value = joinUrl.value.trim();
if (!value) {
joinStatus.textContent = `[${stamp()}] Paste a Roblox join URL first.`;
return;
}
const isRobloxUrl = /^https?:\/\/(www\.)?roblox\.com\//i.test(value);
if (!isRobloxUrl) {
joinStatus.textContent = `[${stamp()}] Invalid URL. Use a https://www.roblox.com/... link.`;
return;
}
window.open(value, '_blank', 'noopener,noreferrer');
joinStatus.textContent = `[${stamp()}] Opened link. You can join only if that user has joins enabled.`;
});

friendsPageBtn.addEventListener('click', () => {
clickAnim(friendsPageBtn);
window.open('https://www.roblox.com/users/friends', '_blank', 'noopener,noreferrer');
joinStatus.textContent = `[${stamp()}] Opened Roblox Friends page.`;
});
</script>
</body>
</html>
Loading