Compare commits

...

47 commits
v2.0.1 ... main

Author SHA1 Message Date
gardenGnostic a42c1a9b0b
Merge pull request #254 from Ratintosh/assets-refactor
Move static assets into assets/ directory
2026-04-17 17:32:10 +02:00
Ratintosh cb4c62aff2 Moved images and audio into assets folder, updated paths accordingly 2026-03-27 01:02:44 -04:00
gardenGnostic e733a250ec
Update README.md 2026-03-16 14:41:09 +01:00
gardenGnostic aa7c257553
Merge pull request #209 from nothingman4ik/feat-fullscreen-new
feat: add fullscreen launch option to new version
2026-03-14 13:30:57 +01:00
nothingman4ik 3e094c6a1d feat: add fullscreen launch option to new version 2026-03-14 14:18:13 +02:00
gardenGnostic 90d40ba2af
Merge pull request #204 from rubiidev18alt/main
Add loading circle + better looking downloading/connecting text
2026-03-14 11:36:43 +01:00
rubiidev18alt c514c981c7
Merge pull request #7 from rubiidev18alt/codex/add-2d-mode-to-skin-renderer
Add 2D/3D toggle for main skin viewer and UI button
2026-03-13 16:50:12 -07:00
rubiidev18alt 4858f83ea1 Keep 2D skin mode fully static 2026-03-13 16:49:42 -07:00
rubiidev18alt 124fb9dbcb
Merge pull request #6 from rubiidev18alt/codex/add-loading-circle-next-to-downloading-text
Add animated loader/progress UI and smarter progress text parsing
2026-03-13 16:28:17 -07:00
rubiidev18alt 8c36195731 Left-align download status row and shrink spinner 2026-03-13 16:27:48 -07:00
gardenGnostic 1c1fbc3d46
Update package.json 2026-03-12 23:10:55 +01:00
gardenGnostic 1e27d63fae
Update release.yml 2026-03-12 23:10:34 +01:00
gardenGnostic 8e05e6b29d
Update release.yml 2026-03-12 23:05:44 +01:00
gardenGnostic 033315526c
v3.5.0 2026-03-12 22:58:49 +01:00
gardenGnostic 1377ece419
Delete JDSherbert - Ultimate UI SFX Pack - Select - 1.mp3 2026-03-12 22:57:23 +01:00
gardenGnostic 38092692a7
Delete JDSherbert - Ultimate UI SFX Pack - Popup Open - 1.mp3 2026-03-12 22:57:15 +01:00
gardenGnostic b54331475a
Delete JDSherbert - Ultimate UI SFX Pack - Popup Close - 1.mp3 2026-03-12 22:57:08 +01:00
gardenGnostic 6cda5ad9ac
Delete JDSherbert - Ultimate UI SFX Pack - Error - 1.mp3 2026-03-12 22:56:59 +01:00
gardenGnostic 9c189b56df
Delete JDSherbert - Ultimate UI SFX Pack - Cursor - 1.mp3 2026-03-12 22:56:51 +01:00
gardenGnostic 32b2909fee
Delete JDSherbert - Ultimate UI SFX Pack - Cancel - 1.mp3 2026-03-12 22:56:45 +01:00
gardenGnostic 4c42fd0e55
Update release.yml 2026-03-12 22:44:58 +01:00
gardenGnostic 3a104398f4
Merge pull request #171 from rubiidev18alt/main
Add Steam Deck UI mode, repo presets, and stronger hover glow (#2)
2026-03-12 14:28:09 +01:00
rubiidev18alt bb721c5ae0
Merge pull request #5 from rubiidev18alt/codex/integrate-controller-button-assets-into-ui
Add controller layout preset UI with interactions and sprite attribution
2026-03-12 00:02:01 -07:00
rubiidev18alt 08c332ecb7 Use exact spritesheet frame coords for controller layout icons 2026-03-11 23:56:53 -07:00
rubiidev18alt 0a06148338
Controller button sprites for ui 2026-03-11 23:01:30 -07:00
rubiidev18alt a8a972a0f6
Merge branch 'gradenGnostic:main' into main 2026-03-11 23:00:58 -07:00
rubiidev18alt 9d3e4fcc57
Add controller layout setting and guide-button quit (#3) 2026-03-11 20:09:16 -07:00
rubiidev18alt 25116b6eba Add controller layout setting and guide-button quit 2026-03-11 20:00:30 -07:00
rubiidev18alt e44695860c
Add Steam Deck UI mode, repo presets, and stronger hover glow (#2) 2026-03-11 19:32:58 -07:00
gardenGnostic 6c59ad20d6
Merge pull request #167 from rubiidev18alt/main
Better accuracy to steam big picture mode
2026-03-12 00:10:16 +01:00
rubiidev18alt 3355244dc4
Merge pull request #1 from rubiidev18alt/codex/integrate-ui-sounds-and-enhance-console-ui
Add UI sound effects, Steam Deck fullscreen support, Proton detection improvements, and UI polish
2026-03-11 15:38:37 -07:00
rubiidev18alt e1a550674a Refine Linux compat scan to better detect Proton-GE runtimes 2026-03-11 15:37:45 -07:00
rubiidev18alt 12b8a503d9
Placeholder sounds
Thanks JDSherbert on itch!
2026-03-11 14:54:20 -07:00
gardenGnostic 3a5b37a8ca
Bump version from 3.0.0 to 3.0.1 2026-03-10 15:56:56 +01:00
gardenGnostic 8f1aec38be
Merge pull request #121 from dtentiion/main
Fix UI overflow, add classic MC launcher UI, text truncation
2026-03-10 10:29:56 +01:00
dtentiion dceef71f87 Fix UI overflow, add classic MC launcher UI, text truncation 2026-03-09 19:46:24 +00:00
gardenGnostic 7b4942ee7e
v3.0.0 2026-03-09 16:34:54 +01:00
gardenGnostic 1937c941ea
v3.0.0 2026-03-09 16:32:42 +01:00
gardenGnostic c50b36e50d
Bump version from 2.9.0 to 2.9.1 2026-03-09 14:08:32 +01:00
gardenGnostic 0fa8df9a21
Add support for building macOS DMG for Intel 2026-03-09 14:08:02 +01:00
gardenGnostic 68f77fc77a
v2.9.0 2026-03-08 22:03:45 +01:00
gardenGnostic 32b02f11f8
Replace old image with new image in README
Updated the image in the README to a new version.
2026-03-08 21:58:26 +01:00
gardenGnostic 46a484d5df
v2.2.0 2026-03-07 23:41:21 +01:00
gardenGnostic 69a0ebbfce
Update README.md 2026-03-07 22:47:21 +01:00
gardenGnostic c37a43449f
Add star history chart with responsive design
Added responsive star history chart to README.
2026-03-07 22:46:48 +01:00
gardenGnostic c9ba08d5d2
Add star history chart to README
Added a star history chart to the README for better visibility of project popularity.
2026-03-07 22:46:06 +01:00
gardenGnostic acb1c41d8d
Bump version from 2.0.0 to 2.0.2 2026-03-07 12:38:55 +01:00
20 changed files with 3768 additions and 592 deletions

View file

@ -33,7 +33,6 @@ jobs:
name: linux-dist
path: dist/*.AppImage
if-no-files-found: error
build-windows:
name: Build Windows Installer
runs-on: windows-latest
@ -59,9 +58,8 @@ jobs:
dist/*.exe
dist/*.msi
if-no-files-found: error
build-mac:
name: Build macOS DMG
name: Build macOS DMG (Apple Silicon)
runs-on: macos-latest
steps:
- name: Checkout
@ -83,10 +81,32 @@ jobs:
name: mac-dist
path: dist/*.dmg
if-no-files-found: error
build-mac-intel:
name: Build macOS DMG (Intel)
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Build macOS DMG (Intel)
run: npm run dist:mac -- --x64
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload macOS Intel artifact
uses: actions/upload-artifact@v4
with:
name: mac-intel-dist
path: dist/*.dmg
if-no-files-found: error
release:
name: Create GitHub Release
needs: [build-linux, build-windows, build-mac]
needs: [build-linux, build-windows, build-mac, build-mac-intel]
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
@ -104,5 +124,6 @@ jobs:
artifacts/linux-dist/*
artifacts/windows-dist/*
artifacts/mac-dist/*
artifacts/mac-intel-dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules/

View file

@ -2,7 +2,10 @@
A custom launcher for Minecraft Legacy Console Edition.
<img width="1278" height="709" alt="image" src="https://github.com/user-attachments/assets/b28cf6e0-150a-4790-b30a-04548a123295" />
<img width="1277" height="717" alt="image" src="https://github.com/user-attachments/assets/eaa9bae6-3b3b-4e39-a3c1-156e34abf3cc" />
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/gradengnostic)
## Features
@ -76,6 +79,11 @@ The launcher supports several compatibility options for Linux:
- **extract-zip**: ZIP archive extraction
- **Tailwind CSS**: UI styling (via CDN)
## Assets
- Controller button sprites: [greatdocbrown](https://greatdocbrown.itch.io/gamepad-ui)
- UI Sounds: Using the free version of [JDSherbert's Ultimate UI SFX Pack on itch.io](https://jdsherbert.itch.io/ultimate-ui-sfx-pack)
## Development
The launcher is built with:
@ -101,3 +109,13 @@ The launcher is built with:
## Contributing
Feel free to submit issues and pull requests for improvements.</content>
## Star History
<a href="https://www.star-history.com/?repos=gradenGnostic%2FLegacyLauncher&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=gradenGnostic/LegacyLauncher&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=gradenGnostic/LegacyLauncher&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=gradenGnostic/LegacyLauncher&type=date&legend=top-left" />
</picture>
</a>

View file

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

BIN
assets/Click_stereo.ogg.mp3 Normal file

Binary file not shown.

BIN
assets/gdb-keyboard-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/gdb-switch-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
assets/gdb-xbox-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

Before

Width:  |  Height:  |  Size: 987 KiB

After

Width:  |  Height:  |  Size: 987 KiB

View file

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 320 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LegacyLauncher</title>
<link rel="icon" type="image/png" href="512x512.png">
<link rel="icon" type="image/png" href="assets/512x512.png">
<link rel="stylesheet" href="style.css">
<script src="https://cdn.tailwindcss.com"></script>
</head>
@ -13,54 +13,92 @@
<div class="title-bar">
<div class="title-bar-text">LegacyLauncher</div>
<div class="window-controls">
<div class="win-btn" onclick="minimizeWindow()"></div>
<div class="win-btn" id="maximize-btn" onclick="toggleMaximize()"></div>
<div class="win-btn close" onclick="closeWindow()">×</div>
<div class="win-btn nav-item" onclick="minimizeWindow()" tabindex="0" title="Minimize"></div>
<div class="win-btn nav-item" id="maximize-btn" onclick="toggleMaximize()" tabindex="0" title="Maximize / Restore"></div>
<div class="win-btn close nav-item" onclick="closeWindow()" tabindex="0" title="Close">×</div>
</div>
</div>
<div id="loader">
<div class="loader-content">
<div class="loader-spinner"></div>
<div id="loader-text">CONNECTING...</div>
<div id="loader-text"><span id="loader-text-label">CONNECTING</span><span class="loading-dots animate" id="loader-dots" aria-hidden="true"><span>.</span><span>.</span><span>.</span></span></div>
</div>
</div>
<div id="offline-indicator" style="display: none; position: fixed; top: 40px; left: 50%; transform: translateX(-50%); background: #c42b1c; color: #fff; padding: 5px 20px; border: 2px solid #000; z-index: 100; font-weight: bold; font-size: 14px; box-shadow: 0 4px 10px rgba(0,0,0,0.5);">
OFFLINE MODE — UPDATES DISABLED
</div>
<div class="main-wrapper">
<div class="sidebar">
<div class="sidebar-title">PATCH NOTES</div>
<div class="sidebar collapsed" id="main-sidebar">
<div class="sidebar-title" onclick="toggleSidebar()">
<span id="sidebar-title-text">PATCH NOTES</span>
<span id="sidebar-toggle-icon" title="Expand Patch Notes"></span>
</div>
<div id="updates-list">
<div class="update-item">Loading updates...</div>
</div>
</div>
<div class="content-area">
<img src="minecraftlogo.png" class="mc-logo-img" alt="Minecraft Logo">
<div id="btn-play-main" class="btn-mc btn-play nav-item" onclick="launchGame()" tabindex="0">PLAY</div>
<div class="version-row">
<span class="version-label">Version:</span>
<div id="version-select-box" class="version-select-box nav-item" tabindex="0">
<span id="current-version-display">Loading...</span>
<div class="select-arrow"></div>
<select id="version-select" class="hidden-select" onchange="updateSelectedRelease()">
<option>Loading...</option>
</select>
</div>
<div id="btn-check-update" class="btn-mc btn-mini nav-item" onclick="checkForUpdatesManual()" title="Check for Updates" tabindex="0">
<img src="restart.png" alt="Update">
<div class="content-area" style="flex-direction: row; align-items: stretch; padding: 0;">
<!-- Classic Mode: Logo + Splash (only visible in classic theme) -->
<div class="classic-logo-area" id="classic-logo-area">
<div style="position: relative;">
<img src="assets/minecraftlogo.png" class="mc-logo-img" alt="Minecraft Logo" style="margin-bottom: 0; margin-top: 0;">
<div id="classic-splash-text" class="splash-text">Splash!</div>
</div>
</div>
<div class="flex gap-4 w-[500px] max-w-[90%]">
<div class="btn-mc flex-grow nav-item" id="btn-profile" onclick="toggleProfile(true)" tabindex="0">PROFILE</div>
<div class="btn-mc flex-grow nav-item" id="btn-servers" onclick="toggleServers(true)" tabindex="0">SERVERS</div>
<div class="btn-mc flex-grow nav-item" id="btn-options" onclick="toggleOptions(true)" tabindex="0">OPTIONS</div>
<!-- Left Side: Main Menu -->
<div class="menu-column" style="flex: 2; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; z-index: 5;">
<div class="relative">
<img src="assets/minecraftlogo.png" class="mc-logo-img" alt="Minecraft Logo">
<div id="splash-text" class="splash-text">Splash!</div>
</div>
<div id="btn-play-main" class="btn-mc btn-play nav-item" onclick="launchGame()" tabindex="0">PLAY</div>
<div class="version-row">
<span class="version-label">Version:</span>
<div id="version-select-box" class="version-select-box nav-item" tabindex="0">
<span id="current-version-display">Loading...</span>
<div class="select-arrow"></div>
<select id="version-select" class="hidden-select" onchange="updateSelectedRelease()">
<option>Loading...</option>
</select>
</div>
<div id="btn-check-update" class="btn-mc btn-mini nav-item" onclick="checkForUpdatesManual()" title="Check for Updates" tabindex="0">
<img src="assets/restart.png" alt="Update">
</div>
</div>
<div class="flex gap-4 w-[500px] max-w-[90%]">
<div class="btn-mc flex-grow nav-item" id="btn-instances" onclick="toggleInstances(true)" tabindex="0" style="font-size: 18px;">INSTANCES</div>
<div class="btn-mc flex-grow nav-item" id="btn-profile" onclick="toggleProfile(true)" tabindex="0" style="font-size: 18px;">PROFILE</div>
<div class="btn-mc flex-grow nav-item" id="btn-servers" onclick="toggleServers(true)" tabindex="0" style="font-size: 18px;">SERVERS</div>
<div class="btn-mc flex-grow nav-item" id="btn-options" onclick="toggleOptions(true)" tabindex="0" style="font-size: 18px;">OPTIONS</div>
</div>
</div>
<!-- Right Side: Skin Viewer -->
<div class="skin-column" style="flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; min-width: 300px; padding-right: 0px;">
<div id="main-skin-viewer" style="width: 100%; height: 500px; cursor: grab; display: flex; align-items: center; justify-content: center;"></div>
<div class="skin-action-row" style="margin-top: -20px; z-index: 10; margin-left: auto; margin-right: auto;">
<div class="btn-mc nav-item skin-action-btn skin-action-btn-main" onclick="openSkinManager()" tabindex="0">
CHANGE SKIN
</div>
<div id="btn-skin-render-mode" class="btn-mc nav-item skin-action-btn skin-action-btn-mode" onclick="toggleMainSkinRenderMode()" tabindex="0">3D</div>
</div>
</div>
<div class="progress-container" id="progress-container">
<div class="progress-text" id="progress-text">Downloading...</div>
<div class="progress-text-wrap" id="progress-text-wrap">
<span class="loader-spinner progress-spinner" id="progress-spinner" aria-hidden="true"></span>
<div class="progress-text" id="progress-text"><span id="progress-text-label">Downloading</span><span class="loading-dots" id="progress-dots" aria-hidden="true"><span>.</span><span>.</span><span>.</span></span><span id="progress-text-suffix"></span></div>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill" id="progress-bar-fill"></div>
</div>
@ -69,7 +107,7 @@
</div>
<div class="modal-overlay" id="servers-modal">
<div class="modal-box" style="width: 850px;">
<div class="modal-box">
<div class="modal-title">CUSTOM SERVERS</div>
<div id="servers-list-container" class="w-full max-h-[300px] overflow-y-auto mb-6 border-2 border-[#555] bg-black p-2">
@ -100,13 +138,84 @@
</div>
</div>
<div class="modal-overlay" id="instances-modal">
<div class="modal-box" style="max-width: 900px;">
<div class="modal-title">GAME INSTANCES</div>
<div id="instances-list-container" class="w-full max-h-[400px] overflow-y-auto mb-6 border-2 border-[#555] bg-black p-2">
<!-- Instances will be listed here -->
<div class="text-center text-gray-400 py-4">No instances found.</div>
</div>
<div class="flex gap-4 w-full">
<div class="btn-mc flex-grow nav-item" onclick="createNewInstance()" tabindex="0">ADD INSTANCE</div>
<div class="btn-mc flex-grow nav-item" onclick="toggleInstances(false)" tabindex="0">DONE</div>
</div>
</div>
</div>
<div class="modal-overlay" id="add-instance-modal">
<div class="modal-box" style="max-width: 600px;">
<div class="modal-title" style="font-size: 32px;">NEW INSTANCE</div>
<div class="mc-input-group">
<label class="mc-label">Instance Name:</label>
<input type="text" id="new-instance-name" class="mc-input nav-item" placeholder="My Instance" tabindex="0">
</div>
<div class="mc-input-group">
<label class="mc-label">GitHub Repo (user/repo):</label>
<input type="text" id="new-instance-repo" class="mc-input nav-item" placeholder="smartcmd/MinecraftConsoles" tabindex="0">
</div>
<div class="flex gap-4 w-full mt-4">
<div class="btn-mc flex-grow nav-item" onclick="saveNewInstance()" tabindex="0">CREATE</div>
<div class="btn-mc flex-grow nav-item" onclick="toggleAddInstance(false)" tabindex="0">CANCEL</div>
</div>
</div>
</div>
<div class="modal-overlay" id="snapshots-modal">
<div class="modal-box" style="max-width: 800px;">
<div class="modal-title">SNAPSHOTS / ROLLBACK</div>
<div id="snapshot-instance-name" style="color: #aaa; text-align: center; margin-top: -15px; margin-bottom: 20px; text-transform: uppercase;">Instance Name</div>
<div id="snapshots-list-container" class="w-full max-h-[350px] overflow-y-auto mb-6 border-2 border-[#555] bg-black p-2">
<div class="text-center text-gray-400 py-4">No snapshots found.</div>
</div>
<div class="flex gap-4 w-full">
<div class="btn-mc flex-grow nav-item" onclick="createSnapshotManual()" tabindex="0">NEW SNAPSHOT</div>
<div class="btn-mc flex-grow nav-item" onclick="toggleSnapshots(false)" tabindex="0">DONE</div>
</div>
</div>
</div>
<div class="modal-overlay" id="options-modal">
<div class="modal-box">
<div class="modal-title">LAUNCHER OPTIONS</div>
<div class="mc-input-group">
<label class="mc-label">Installation Directory:</label>
<div class="flex gap-2 mb-2">
<input type="text" id="install-path-input" class="mc-input nav-item !mb-0" placeholder="Default: Documents/LegacyClient" tabindex="0">
<div class="btn-mc btn-mini nav-item !w-[60px] !h-[48px]" onclick="browseInstallDir()" tabindex="0">...</div>
</div>
<div class="flex gap-2">
<div class="btn-mc flex-grow !h-[40px] !text-lg !mb-0 nav-item" onclick="openGameDir()" tabindex="0">GAME FOLDER</div>
<div class="btn-mc flex-grow !h-[40px] !text-lg !mb-0 nav-item" onclick="openScreenshotsGallery()" tabindex="0">SCREENSHOTS</div>
</div>
</div>
<div class="mc-input-group">
<label class="mc-label">GitHub Repository Source:</label>
<input type="text" id="repo-input" class="mc-input nav-item" placeholder="user/repo" tabindex="0">
<label class="mc-label" style="margin-top: 10px;">Repository Preset:</label>
<select id="repo-preset-select" class="mc-input nav-item" onchange="applyRepoPreset()" tabindex="0" style="height: 48px; margin-top: 8px;">
<option value="custom">Custom</option>
<option value="smartcmd/MinecraftConsoles">smartcmd (default)</option>
<option value="cath0degaytube/MinecraftConsoles">cath0degaytube (no watermark)</option>
</select>
</div>
<div class="mc-input-group">
@ -125,6 +234,19 @@
</div>
</div>
<div class="mc-input-group" id="custom-proton-group" style="display: none;">
<label class="mc-label">Custom Proton Path:</label>
<input type="text" id="custom-proton-path" class="mc-input nav-item" placeholder="/path/to/proton" tabindex="0">
</div>
<div class="mc-input-group">
<label class="mc-label">Launcher Music Volume:</label>
<div class="mc-slider-container">
<input type="range" id="volume-slider" min="0" max="1" step="0.01" value="1" class="mc-slider nav-item w-full" tabindex="0">
<div id="volume-percent" class="mc-slider-percent">100%</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4 w-full mt-2">
<div class="mc-input-group">
<label class="mc-label">Connect/Bind IP:</label>
@ -136,6 +258,13 @@
</div>
</div>
<div class="mc-input-group">
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="fullscreen-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
Launch in Fullscreen (-fullscreen)
</label>
</div>
<div class="mc-input-group">
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="server-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
@ -143,6 +272,39 @@
</label>
</div>
<div class="mc-input-group">
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="classic-theme-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
Use Classic Minecraft Launcher UI
</label>
</div>
<div class="mc-input-group">
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="steamdeck-mode-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
Use Steam Deck Optimized UI Mode
</label>
</div>
<div class="mc-input-group">
<label class="mc-label">Controller Confirm/Cancel Layout:</label>
<select id="controller-layout-select" class="mc-input nav-item" tabindex="0" style="height: 48px; margin-top: 8px;">
<option value="auto">Auto Detect (Recommended)</option>
<option value="xbox">Xbox Style (A = Confirm, B = Cancel)</option>
<option value="nintendo">Nintendo Style (B = Confirm, A = Cancel)</option>
</select>
<div id="controller-layout-presets" class="controller-layout-presets" role="radiogroup" aria-label="Controller layout presets">
<button type="button" class="controller-layout-preset nav-item" data-layout="xbox" tabindex="0" aria-pressed="false">
<span class="controller-layout-icon controller-layout-icon-xbox" aria-hidden="true"></span>
<span class="controller-layout-text">Xbox Style (A = Confirm, B = Cancel)</span>
</button>
<button type="button" class="controller-layout-preset nav-item" data-layout="nintendo" tabindex="0" aria-pressed="false">
<span class="controller-layout-icon controller-layout-icon-switch" aria-hidden="true"></span>
<span class="controller-layout-text">Nintendo Style (B = Confirm, A = Cancel)</span>
</button>
</div>
</div>
<div class="flex gap-4 w-full mt-4">
<div id="btn-options-done" class="btn-mc flex-grow nav-item" onclick="saveOptions()" tabindex="0">DONE</div>
<div id="btn-options-cancel" class="btn-mc flex-grow nav-item" onclick="toggleOptions(false)" tabindex="0">CANCEL</div>
@ -150,13 +312,28 @@
</div>
</div>
<div class="modal-overlay" id="gallery-modal">
<div class="modal-box" style="max-width: 1000px; width: 85vw; height: 85vh;">
<div class="modal-title">SCREENSHOTS GALLERY</div>
<div id="gallery-container" class="w-full flex-grow overflow-y-auto mb-6 border-2 border-[#555] bg-black p-4 grid grid-cols-2 md:grid-cols-3 gap-4 auto-rows-max">
<!-- Screenshots will be dynamically loaded here -->
</div>
<div class="flex gap-4 w-full">
<div class="btn-mc flex-grow nav-item" onclick="openScreenshotsDir()" tabindex="0">OPEN FOLDER</div>
<div class="btn-mc flex-grow nav-item" onclick="toggleGallery(false)" tabindex="0">DONE</div>
</div>
</div>
</div>
<div class="modal-overlay" id="profile-modal">
<div class="modal-box">
<div class="modal-title">PLAYER PROFILE</div>
<div class="mc-input-group">
<label class="mc-label">In-Game Username:</label>
<input type="text" id="username-input" class="mc-input nav-item" placeholder="Steve" tabindex="0">
<input type="text" id="username-input" class="mc-input nav-item" placeholder="Steve" tabindex="0" maxlength="16">
</div>
<div class="mc-input-group mt-4">
@ -174,10 +351,7 @@
<div class="modal-overlay" id="update-modal">
<div class="modal-box relative">
<div class="modal-close-btn" id="btn-close-update">×</div>
<div class="modal-title">UPDATE AVAILABLE</div>
<div id="update-modal-text" class="modal-body-text">
A new version is available. Would you like to update now?
</div>
<div id="update-modal-text" class="modal-body-text" style="display: none; margin-bottom: 20px;"></div>
<div class="flex gap-4 w-full">
<div class="btn-mc flex-grow nav-item" id="btn-confirm-update">UPDATE NOW</div>
<div class="btn-mc flex-grow nav-item" id="btn-skip-update">LATER</div>
@ -185,8 +359,107 @@
</div>
</div>
<div class="modal-overlay" id="skin-modal" style="display: none;">
<div class="modal-box" style="max-width: 900px; background: var(--mc-gui-bg); border: 4px solid #000; padding: 30px; box-shadow: inset 4px 4px 0 #fff;">
<div class="modal-title" style="margin-bottom: 25px;">SKIN UPLOADER</div>
<div id="drop-zone" class="nav-item" style="width: 100%; height: 120px; display: flex; align-items: center; justify-content: center; background: #000; border: 2px solid #555; cursor: pointer; margin-bottom: 20px;" tabindex="0">
<input type="file" id="skin-input" accept=".png" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
<div id="upload-prompt" class="text-center">
<p class="text-xl text-white" style="text-shadow: 2px 2px 0 #000;">CLICK OR DRAG SKIN HERE</p>
<p class="text-xs text-gray-400 mt-1 uppercase font-bold" style="letter-spacing: 2px;">64x32 or 64x64 PNG</p>
</div>
</div>
<div id="preview-container" class="hidden flex flex-col items-center w-full">
<div class="flex flex-row gap-6 items-stretch w-full justify-center mb-6" style="height: 400px;">
<!-- 3D Preview Panel -->
<div class="relative bg-black rounded-none border-2 border-[#555] flex flex-col items-center overflow-hidden w-1/2">
<div id="skin-viewer-container" class="w-full h-full"></div>
<div style="position: absolute; bottom: 10px; left: 10px; color: #55ff55; font-size: 14px; text-shadow: 1px 1px 0 #000; z-index: 5;">3D PREVIEW</div>
</div>
<!-- info/2D Panel -->
<div class="flex-1 w-1/2 flex flex-col gap-4">
<div class="bg-black p-4 border-2 border-[#555] flex flex-col items-center flex-1 justify-center">
<div class="relative" style="margin-bottom: 15px;">
<canvas id="skin-canvas" width="64" height="32" class="pixelated" style="width: 256px; height: 128px; image-rendering: pixelated; border: 2px solid #555;"></canvas>
</div>
<div style="color: #aaa; font-size: 14px; text-align: center; text-transform: uppercase;">LEGACY OUTPUT (64x32)</div>
<div id="status-message" style="margin-top: 10px; font-size: 18px;"></div>
</div>
<div style="background: rgba(0,0,0,0.3); padding: 15px; border: 2px solid #555; color: #eee; font-size: 16px;">
<div style="display: flex; justify-content: space-between; border-bottom: 2px solid #555; padding-bottom: 8px; margin-bottom: 8px;">
<span>DETECTED:</span>
<span id="format-label" style="color: #fff;">--</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>TARGET:</span>
<span style="color: #888; font-size: 14px;">Common/res/mob/char.png</span>
</div>
</div>
</div>
</div>
<div id="save-skin-btn" class="btn-mc w-full nav-item" tabindex="0">SAVE SKIN</div>
</div>
<div class="flex justify-center w-full mt-6">
<div id="btn-close-skin" class="btn-mc nav-item !mb-0" onclick="closeSkinManager()" tabindex="0">DONE</div>
</div>
</div>
</div>
<!-- Classic Launcher Bottom Bar (only shown when classic theme is active) -->
<div id="classic-bottom-bar" class="classic-bottom-bar">
<!-- Left: Profile Section -->
<div class="classic-profile-section">
<div class="classic-avatar" id="classic-avatar">S</div>
<div class="classic-profile-info">
<div class="classic-username-label">Logged in as</div>
<div class="classic-username-display" id="classic-username-display">Player</div>
</div>
<div class="classic-mini-btn" onclick="toggleProfile(true)" title="Edit Profile">Edit Profile</div>
<div class="classic-mini-btn" onclick="openSkinManager()" title="Change Skin">Skin</div>
</div>
<!-- Center: Play Button -->
<div class="classic-center-section">
<div class="btn-mc btn-play classic-play-btn nav-item" id="classic-btn-play" onclick="launchGame()" tabindex="0">PLAY</div>
</div>
<!-- Right: Version + Nav -->
<div class="classic-right-section">
<div class="classic-version-row">
<span class="version-label" style="font-size: 16px; white-space: nowrap;">Version:</span>
<div id="classic-version-select-box" class="version-select-box nav-item" tabindex="0" style="height: 40px; font-size: 16px; flex-grow: 1; padding-right: 51px;">
<span id="classic-version-display">Loading...</span>
<div class="select-arrow" style="width: 36px;"></div>
<select id="classic-version-select" class="hidden-select" onchange="syncVersionFromClassic()">
<option>Loading...</option>
</select>
</div>
<div class="btn-mc btn-mini nav-item" onclick="checkForUpdatesManual()" title="Check for Updates" tabindex="0" style="width: 40px !important; height: 40px; max-width: 40px !important;">
<img src="restart.png" alt="Update">
</div>
</div>
<div class="classic-nav-links">
<span class="classic-link nav-item" onclick="toggleInstances(true)" tabindex="0">Instances</span>
<span class="classic-link nav-item" onclick="toggleServers(true)" tabindex="0">Servers</span>
<span class="classic-link nav-item" onclick="toggleOptions(true)" tabindex="0">Options</span>
</div>
</div>
</div>
<div id="toast">NOTIFICATION</div>
<div id="music-toggle" class="music-btn nav-item" onclick="toggleMusic()" title="Toggle Music" tabindex="0">
<span id="music-icon"></span>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="skin_manager.js"></script>
<script src="renderer.js"></script>
</body>
</html>

130
main.js
View file

@ -1,4 +1,4 @@
const { app, BrowserWindow, shell, ipcMain } = require('electron');
const { app, BrowserWindow, shell, ipcMain, dialog, globalShortcut, desktopCapturer } = require('electron');
const path = require('path');
const Store = require('electron-store');
const fs = require('fs');
@ -7,9 +7,11 @@ const extractZip = require('extract-zip');
const { exec } = require('child_process');
const store = new Store();
let mainWindow = null;
let isGameRunning = false;
function createWindow() {
const win = new BrowserWindow({
mainWindow = new BrowserWindow({
width: 1280,
height: 720,
minWidth: 1024,
@ -27,25 +29,131 @@ function createWindow() {
}
});
win.loadFile('index.html');
mainWindow.loadFile('index.html');
ipcMain.on('window-minimize', () => win.minimize());
ipcMain.on('window-minimize', () => mainWindow.minimize());
ipcMain.on('window-maximize', () => {
if (win.isMaximized()) {
win.unmaximize();
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
win.maximize();
mainWindow.maximize();
}
});
ipcMain.on('window-close', () => win.close());
ipcMain.on('window-close', () => mainWindow.close());
ipcMain.on('window-fullscreen', () => {
mainWindow.setFullScreen(!mainWindow.isFullScreen());
});
ipcMain.on('window-set-fullscreen', (event, enabled) => {
mainWindow.setFullScreen(Boolean(enabled));
});
ipcMain.handle('take-screenshot', async (event) => {
try {
const screenshotsDir = path.join(app.getPath('userData'), 'Screenshots');
if (!fs.existsSync(screenshotsDir)) {
fs.mkdirSync(screenshotsDir, { recursive: true });
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `screenshot-${timestamp}.png`;
const filePath = path.join(screenshotsDir, fileName);
if (isGameRunning) {
const sources = await desktopCapturer.getSources({
types: ['window', 'screen'],
thumbnailSize: { width: 3840, height: 2160 } // High res for screenshot
});
// Find Minecraft window or fallback to primary screen
const source = sources.find(s => s.name.toLowerCase().includes('minecraft')) ||
sources.find(s => s.id.startsWith('screen'));
if (source) {
fs.writeFileSync(filePath, source.thumbnail.toPNG());
return filePath;
}
}
// Fallback to launcher capture if game isn't running or not found
const win = BrowserWindow.fromWebContents(event.sender) || mainWindow;
if (!win) throw new Error("Window not found");
const image = await win.capturePage();
fs.writeFileSync(filePath, image.toPNG());
return filePath;
} catch (err) {
console.error("Screenshot capture error:", err);
throw err;
}
});
ipcMain.on('game-running-state', (event, running) => {
isGameRunning = running;
if (running) {
globalShortcut.register('F2', () => {
if (mainWindow) {
mainWindow.webContents.send('trigger-screenshot');
}
});
} else {
globalShortcut.unregister('F2');
}
});
ipcMain.handle('list-screenshots', async () => {
const screenshotsDir = path.join(app.getPath('userData'), 'Screenshots');
if (!fs.existsSync(screenshotsDir)) {
return [];
}
const files = fs.readdirSync(screenshotsDir);
return files
.filter(f => f.toLowerCase().endsWith('.png'))
.sort((a, b) => {
try {
// Extract timestamp from 'screenshot-YYYY-MM-DDTHH-mm-ss-SSSZ.png'
const timeA = a.replace('screenshot-', '').replace('.png', '').replace(/-/g, ':');
const timeB = b.replace('screenshot-', '').replace('.png', '').replace(/-/g, ':');
return new Date(timeB) - new Date(timeA);
} catch (e) {
return 0;
}
})
.map(f => ({
name: f,
path: path.join(screenshotsDir, f),
url: `file://${path.join(screenshotsDir, f)}`
}));
});
ipcMain.handle('delete-screenshot', async (event, fileName) => {
const filePath = path.join(app.getPath('userData'), 'Screenshots', fileName);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
return true;
}
return false;
});
ipcMain.handle('open-screenshots-dir', async () => {
const screenshotsDir = path.join(app.getPath('userData'), 'Screenshots');
if (!fs.existsSync(screenshotsDir)) {
fs.mkdirSync(screenshotsDir, { recursive: true });
}
shell.openPath(screenshotsDir);
});
ipcMain.handle('store-get', (event, key) => store.get(key));
ipcMain.handle('store-set', (event, key, value) => store.set(key, value));
ipcMain.handle('select-directory', async () => {
const result = await dialog.showOpenDialog({
properties: ['openDirectory', 'createDirectory']
});
return result.filePaths[0];
});
win.on('maximize', () => win.webContents.send('window-is-maximized', true));
win.on('unmaximize', () => win.webContents.send('window-is-maximized', false));
mainWindow.on('maximize', () => mainWindow.webContents.send('window-is-maximized', true));
mainWindow.on('unmaximize', () => mainWindow.webContents.send('window-is-maximized', false));
win.webContents.setWindowOpenHandler(({ url }) => {
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});

10
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "legacylauncher",
"version": "1.1.1",
"version": "3.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "legacylauncher",
"version": "1.1.1",
"version": "3.5.0",
"license": "ISC",
"dependencies": {
"electron-store": "^6.0.1",
@ -4690,9 +4690,9 @@
}
},
"node_modules/tar": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz",
"integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==",
"version": "7.5.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {

View file

@ -1,7 +1,7 @@
{
"name": "legacylauncher",
"version": "2.0.0",
"description": "",
"version": "3.5.0",
"description": "A Minecraft: Legacy Console Edition launcher",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
@ -30,7 +30,17 @@
"flatpak"
],
"category": "Game",
"icon": "512x512.png"
"icon": "512x512.png",
"desktop": {
"entry": {
"Name": "Minecraft LCE Launcher",
"GenericName": "Minecraft Launcher",
"Comment": "A Minecraft: Legacy Console Edition launcher",
"Categories": "Game;Emulation;",
"StartupNotify": "true"
}
},
"executableName": "legacylauncher"
},
"mac": {
"target": [

File diff suppressed because it is too large Load diff

469
skin_manager.js Normal file
View file

@ -0,0 +1,469 @@
// skin_manager.js
// Handles skin uploading, conversion, 3D preview, and saving to char.png
let mainMenuScene, mainMenuCamera, mainMenuRenderer, mainMenuPlayerGroup;
let isMainSkinDragging = false;
let mainMenuSkinRenderMode = '3d';
let skinScene, skinCamera, skinRenderer, skinPlayerGroup;
let isSkinDragging = false;
let previousSkinMousePosition = { x: 0, y: 0 };
let processedSkinDataUrl = null;
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
const skinInput = document.getElementById('skin-input');
const dropZone = document.getElementById('drop-zone');
const saveSkinBtn = document.getElementById('save-skin-btn');
const closeSkinBtn = document.getElementById('btn-close-skin');
if (skinInput) skinInput.addEventListener('change', (e) => handleSkinFile(e.target.files[0]));
if (dropZone) {
dropZone.addEventListener('click', () => skinInput?.click());
dropZone.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
skinInput?.click();
}
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-green-500');
});
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('border-green-500'));
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-green-500');
handleSkinFile(e.dataTransfer.files[0]);
});
}
if (saveSkinBtn) {
saveSkinBtn.addEventListener('click', saveSkinToDisk);
}
if (closeSkinBtn) {
closeSkinBtn.addEventListener('click', closeSkinManager);
}
// Initialize Main Menu Viewer
initMainMenuSkinViewer();
});
function initMainMenuSkinViewer() {
const container = document.getElementById('main-skin-viewer');
if (!container) return;
mainMenuScene = new THREE.Scene();
mainMenuCamera = new THREE.PerspectiveCamera(45, container.offsetWidth / container.offsetHeight, 0.1, 1000);
mainMenuCamera.position.set(0, 5, 60);
mainMenuRenderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
mainMenuRenderer.setSize(container.offsetWidth, container.offsetHeight);
mainMenuRenderer.setPixelRatio(window.devicePixelRatio);
mainMenuRenderer.outputEncoding = THREE.sRGBEncoding;
container.appendChild(mainMenuRenderer.domElement);
mainMenuScene.add(new THREE.AmbientLight(0xffffff, 1.1));
const dl = new THREE.DirectionalLight(0xffffff, 0.5);
dl.position.set(10, 20, 15);
mainMenuScene.add(dl);
mainMenuPlayerGroup = new THREE.Group();
mainMenuScene.add(mainMenuPlayerGroup);
// Interaction for Main Menu Viewer
let prevX = 0;
container.addEventListener('mousedown', (e) => {
if (mainMenuSkinRenderMode !== '3d') return;
isMainSkinDragging = true;
prevX = e.clientX;
});
window.addEventListener('mouseup', () => isMainSkinDragging = false);
window.addEventListener('mousemove', (e) => {
if (mainMenuSkinRenderMode === '3d' && isMainSkinDragging && mainMenuPlayerGroup) {
mainMenuPlayerGroup.rotation.y += (e.clientX - prevX) * 0.01;
prevX = e.clientX;
}
});
// Auto-rotate slowly
function animateMain() {
requestAnimationFrame(animateMain);
if (mainMenuSkinRenderMode === '3d' && !isMainSkinDragging && mainMenuPlayerGroup) {
mainMenuPlayerGroup.rotation.y += 0.005;
}
if (mainMenuRenderer && mainMenuScene && mainMenuCamera) mainMenuRenderer.render(mainMenuScene, mainMenuCamera);
}
animateMain();
// Load current skin after short delay to ensure paths are ready
setTimeout(loadMainMenuSkin, 500);
// Handle window resize
window.addEventListener('resize', () => {
if (mainMenuCamera && mainMenuRenderer && container) {
mainMenuCamera.aspect = container.offsetWidth / container.offsetHeight;
mainMenuCamera.updateProjectionMatrix();
mainMenuRenderer.setSize(container.offsetWidth, container.offsetHeight);
}
});
}
async function loadMainMenuSkin() {
try {
// Ensure install dir is available (currentInstance might not be ready)
const installDir = await window.getInstallDir();
if (!installDir) return;
const skinPath = path.join(installDir, 'Common', 'res', 'mob', 'char.png');
if (fs.existsSync(skinPath)) {
const skinData = fs.readFileSync(skinPath);
const blob = new Blob([skinData]);
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const isLegacy = img.height === 32;
updateSkinModel(img.src, isLegacy, mainMenuPlayerGroup, mainMenuSkinRenderMode);
};
img.src = url;
} else {
console.log("No skin found at " + skinPath);
}
} catch (e) {
console.warn("Could not load main menu skin (startup race condition?):", e);
}
}
function toggleMainSkinRenderMode() {
mainMenuSkinRenderMode = mainMenuSkinRenderMode === '3d' ? '2d' : '3d';
const modeButton = document.getElementById('btn-skin-render-mode');
if (modeButton) modeButton.textContent = mainMenuSkinRenderMode.toUpperCase();
const container = document.getElementById('main-skin-viewer');
if (container) {
container.style.cursor = mainMenuSkinRenderMode === '3d' ? 'grab' : 'default';
}
isMainSkinDragging = false;
if (mainMenuSkinRenderMode === '2d' && mainMenuPlayerGroup) {
mainMenuPlayerGroup.rotation.y = 0;
}
loadMainMenuSkin();
}
function updateSkinModel(dataUrl, isLegacy, targetGroup, renderMode = '3d') {
if (!targetGroup) return;
new THREE.TextureLoader().load(dataUrl, (texture) => {
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
texture.encoding = THREE.sRGBEncoding;
// Clear existing children
while(targetGroup.children.length > 0) targetGroup.remove(targetGroup.children[0]);
const createFaceMaterial = (tex, x, y, w, h) => {
const texWidth = tex.image.width;
const texHeight = tex.image.height;
const matTex = tex.clone();
matTex.magFilter = THREE.NearestFilter;
matTex.minFilter = THREE.NearestFilter;
matTex.repeat.set(w / texWidth, h / texHeight);
matTex.offset.set(x / texWidth, 1 - (y + h) / texHeight);
matTex.needsUpdate = true;
return new THREE.MeshLambertMaterial({ map: matTex, transparent: true, alphaTest: 0.5, side: THREE.FrontSide });
};
const createBodyPart = (w, h, d, tex, uv, offset = 0) => {
const geometry = new THREE.BoxGeometry(w, h, d);
const materials = [
createFaceMaterial(tex, uv.left[0], uv.left[1], uv.left[2], uv.left[3]), // Left (Standard MC "Right")
createFaceMaterial(tex, uv.right[0], uv.right[1], uv.right[2], uv.right[3]), // Right (Standard MC "Left")
createFaceMaterial(tex, uv.top[0], uv.top[1], uv.top[2], uv.top[3]),
createFaceMaterial(tex, uv.bottom[0], uv.bottom[1], uv.bottom[2], uv.bottom[3]),
createFaceMaterial(tex, uv.front[0], uv.front[1], uv.front[2], uv.front[3]),
createFaceMaterial(tex, uv.back[0], uv.back[1], uv.back[2], uv.back[3])
];
const mesh = new THREE.Mesh(geometry, materials);
if (offset !== 0) mesh.scale.set(1 + offset, 1 + offset, 1 + offset);
return mesh;
};
const limbUv = (x, y) => ({
top: [x+4, y, 4, 4], bottom: [x+8, y, 4, 4],
right: [x, y+4, 4, 12], front: [x+4, y+4, 4, 12],
left: [x+8, y+4, 4, 12], back: [x+12, y+4, 4, 12]
});
const create2DPart = (tex, uvFront, width, height, x, y, scale = 1) => {
const texWidth = tex.image.width;
const texHeight = tex.image.height;
const partTex = tex.clone();
partTex.magFilter = THREE.NearestFilter;
partTex.minFilter = THREE.NearestFilter;
partTex.repeat.set(uvFront[2] / texWidth, uvFront[3] / texHeight);
partTex.offset.set(uvFront[0] / texWidth, 1 - (uvFront[1] + uvFront[3]) / texHeight);
partTex.needsUpdate = true;
const geometry = new THREE.PlaneGeometry(width, height);
const material = new THREE.MeshBasicMaterial({ map: partTex, transparent: true, alphaTest: 0.5, side: THREE.DoubleSide });
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, y, 0);
mesh.scale.set(scale, scale, 1);
return mesh;
};
if (renderMode === '2d') {
const frontParts = [
create2DPart(texture, [8, 8, 8, 8], 8, 8, 0, 10),
create2DPart(texture, [40, 8, 8, 8], 8, 8, 0, 10, 1.12),
create2DPart(texture, [20, 20, 8, 12], 8, 12, 0, 0),
create2DPart(texture, [44, 20, 4, 12], 4, 12, -6, 0),
create2DPart(texture, isLegacy ? [44, 20, 4, 12] : [36, 52, 4, 12], 4, 12, 6, 0),
create2DPart(texture, [4, 20, 4, 12], 4, 12, -2, -12),
create2DPart(texture, isLegacy ? [4, 20, 4, 12] : [20, 52, 4, 12], 4, 12, 2, -12)
];
if (!isLegacy) {
frontParts.push(
create2DPart(texture, [20, 36, 8, 12], 8, 12, 0, 0, 1.05),
create2DPart(texture, [44, 36, 4, 12], 4, 12, -6, 0, 1.05),
create2DPart(texture, [52, 52, 4, 12], 4, 12, 6, 0, 1.05),
create2DPart(texture, [4, 36, 4, 12], 4, 12, -2, -12, 1.05),
create2DPart(texture, [4, 52, 4, 12], 4, 12, 2, -12, 1.05)
);
}
frontParts.forEach((part) => targetGroup.add(part));
targetGroup.rotation.y = 0;
return;
}
// Head
const headUvs = { top: [8, 0, 8, 8], bottom: [16, 0, 8, 8], right: [0, 8, 8, 8], left: [16, 8, 8, 8], front: [8, 8, 8, 8], back: [24, 8, 8, 8] };
const head = createBodyPart(8, 8, 8, texture, headUvs);
head.position.y = 10;
targetGroup.add(head);
// Hat
const hatUvs = { top: [40, 0, 8, 8], bottom: [48, 0, 8, 8], right: [32, 8, 8, 8], left: [48, 8, 8, 8], front: [40, 8, 8, 8], back: [56, 8, 8, 8] };
const hat = createBodyPart(8, 8, 8, texture, hatUvs, 0.12);
hat.position.y = 10;
targetGroup.add(hat);
// Torso
const torsoUvs = { top: [20, 16, 8, 4], bottom: [28, 16, 8, 4], right: [16, 20, 4, 12], left: [28, 20, 4, 12], front: [20, 20, 8, 12], back: [32, 20, 8, 12] };
targetGroup.add(createBodyPart(8, 12, 4, texture, torsoUvs));
// Jacket (non-legacy only)
if (!isLegacy) {
const jacketUvs = { top: [20, 32, 8, 4], bottom: [28, 32, 8, 4], right: [16, 36, 4, 12], left: [28, 36, 4, 12], front: [20, 36, 8, 12], back: [32, 36, 8, 12] };
targetGroup.add(createBodyPart(8, 12, 4, texture, jacketUvs, 0.05));
}
// Limbs
const limbs = [
{ pos: [-6, 0, 0], uv: limbUv(40, 16), layerUv: limbUv(40, 32) },
{ pos: [6, 0, 0], uv: isLegacy ? limbUv(40, 16) : limbUv(32, 48), layerUv: limbUv(48, 48) },
{ pos: [-2, -12, 0], uv: limbUv(0, 16), layerUv: limbUv(0, 32) },
{ pos: [2, -12, 0], uv: isLegacy ? limbUv(0, 16) : limbUv(16, 48), layerUv: limbUv(0, 48) }
];
limbs.forEach(l => {
const base = createBodyPart(4, 12, 4, texture, l.uv);
base.position.set(...l.pos);
targetGroup.add(base);
if (!isLegacy) {
const layer = createBodyPart(4, 12, 4, texture, l.layerUv, 0.05);
layer.position.set(...l.pos);
targetGroup.add(layer);
}
});
targetGroup.rotation.y = 0;
});
}
function openSkinManager() {
const modal = document.getElementById('skin-modal');
modal.style.display = 'flex';
modal.style.opacity = '1';
// Clear previous state in modal
const previewContainer = document.getElementById('preview-container');
if (previewContainer) previewContainer.classList.add('hidden');
const sysMsg = document.getElementById('sys-message');
if (sysMsg) sysMsg.classList.add('hidden');
// Load current skin into preview (optional, maybe we only want to see uploaded ones)
// loadCurrentSkinToPreview();
}
function closeSkinManager() {
const modal = document.getElementById('skin-modal');
modal.style.opacity = '0';
setTimeout(() => {
modal.style.display = 'none';
// Reset state
const prompt = document.getElementById('upload-prompt');
if (prompt) prompt.style.display = 'block';
const previewContainer = document.getElementById('preview-container');
if (previewContainer) previewContainer.classList.add('hidden');
processedSkinDataUrl = null;
}, 300);
}
function handleSkinFile(file) {
if (!file || !file.type.includes('png')) return window.showToast("Only PNG files are supported!");
const prompt = document.getElementById('upload-prompt');
if (prompt) prompt.style.display = 'none';
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
processSkinImage(img, e.target.result);
};
img.onerror = () => {
window.showToast("Failed to load image");
if(prompt) prompt.style.display = 'block';
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function processSkinImage(img, srcUrl, isInitialLoad = false) {
const canvas = document.getElementById('skin-canvas');
const ctx = canvas.getContext('2d');
const formatLabel = document.getElementById('format-label');
const statusMessage = document.getElementById('status-message');
const previewContainer = document.getElementById('preview-container');
const saveBtn = document.getElementById('save-skin-btn');
const prompt = document.getElementById('upload-prompt');
if (img.width !== 64) {
if(prompt) prompt.style.display = 'block';
if(previewContainer) previewContainer.classList.add('hidden');
return window.showToast("Invalid skin width. Must be 64px.");
}
if(prompt) prompt.style.display = 'none';
const isLegacy = img.height === 32;
ctx.clearRect(0, 0, 64, 32);
ctx.drawImage(img, 0, 0, 64, 32, 0, 0, 64, 32);
processedSkinDataUrl = canvas.toDataURL('image/png');
previewContainer.classList.remove('hidden');
if (isInitialLoad) {
if(formatLabel) formatLabel.textContent = "Current Skin";
if(statusMessage) statusMessage.innerHTML = "<span class='text-blue-400 font-black' style='color: #60a5fa;'>LOADED FROM DISK</span>";
if(saveBtn) {
saveBtn.textContent = "SAVED";
saveBtn.classList.add('disabled');
}
} else {
if(formatLabel) formatLabel.textContent = isLegacy ? "64x32 (Legacy)" : "64x64 (Modern)";
if(statusMessage) statusMessage.innerHTML = isLegacy ? "<span class='text-green-400 font-black' style='color: #4ade80;'>LEGACY READY</span>" : "<span class='text-yellow-400 font-black' style='color: #facc15;'>CONVERTED TO 64x32</span>";
if(saveBtn) {
saveBtn.textContent = "SAVE SKIN";
saveBtn.classList.remove('disabled');
}
}
if (!skinScene) initPreviewEngine();
updateSkinModel(srcUrl, isLegacy, skinPlayerGroup, '3d');
}
function initPreviewEngine() {
const container = document.getElementById('skin-viewer-container');
if (!container) return;
skinScene = new THREE.Scene();
skinCamera = new THREE.PerspectiveCamera(35, container.offsetWidth / container.offsetHeight, 0.1, 1000);
skinCamera.position.set(0, 5, 70);
skinRenderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
skinRenderer.setSize(container.offsetWidth, container.offsetHeight);
skinRenderer.setPixelRatio(window.devicePixelRatio);
skinRenderer.outputEncoding = THREE.sRGBEncoding;
container.appendChild(skinRenderer.domElement);
skinScene.add(new THREE.AmbientLight(0xffffff, 0.9));
const dl = new THREE.DirectionalLight(0xffffff, 0.35);
dl.position.set(10, 20, 15);
skinScene.add(dl);
skinPlayerGroup = new THREE.Group();
skinScene.add(skinPlayerGroup);
container.addEventListener('mousedown', () => isSkinDragging = true);
window.addEventListener('mouseup', () => isSkinDragging = false);
window.addEventListener('mousemove', (e) => {
if (isSkinDragging && skinPlayerGroup) {
skinPlayerGroup.rotation.y += (e.movementX) * 0.01;
}
});
function animate() {
requestAnimationFrame(animate);
if (!isSkinDragging && skinPlayerGroup) skinPlayerGroup.rotation.y += 0.008;
if (skinRenderer && skinScene && skinCamera) skinRenderer.render(skinScene, skinCamera);
}
animate();
}
async function saveSkinToDisk() {
if (!processedSkinDataUrl) return;
try {
const installDir = await window.getInstallDir();
const savePath = path.join(installDir, 'Common', 'res', 'mob', 'char.png');
const dir = path.dirname(savePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const base64Data = processedSkinDataUrl.replace(/^data:image\/png;base64,/, "");
fs.writeFileSync(savePath, base64Data, 'base64');
window.showToast("Skin Saved Successfully!");
const saveBtn = document.getElementById('save-skin-btn');
if(saveBtn) {
saveBtn.textContent = "SAVED!";
saveBtn.classList.add('disabled');
}
// Refresh main menu skin
loadMainMenuSkin();
// Close modal after short delay?
setTimeout(closeSkinManager, 1000);
} catch (e) {
window.showToast("Error Saving Skin: " + e.message);
console.error(e);
}
}
// Global Export
window.openSkinManager = openSkinManager;
window.initMainMenuSkinViewer = initMainMenuSkinViewer;
window.loadMainMenuSkin = loadMainMenuSkin;
window.toggleMainSkinRenderMode = toggleMainSkinRenderMode;

483
strings.txt Normal file
View file

@ -0,0 +1,483 @@
Happy birthday, ez!
Happy birthday, Notch!
Let me tell you about Homestuck!
Let me tell you about Homestuck!
Let me tell you about Homestuck!
Merry X-mas!
Happy New Year!
As seen on TV!
A young man stands in his bedroom!
A young man stands in his bedroom!
A young woman stands in her bedroom!
A young woman stands in her bedroom!
Awesome!
100% pure!
Better than Prey!
More polygons!
Get the cruxite dowel!
Get the cruxite dowel!
Get the cruxite dowel!
Flashing letters!
Made by Notch!
It's here!
Best in class!
It's finished!
Kind of dragon free!
Excitement!
Where doing it man, where making this hapen!
Where doing it man, where making this hapen!
One of a kind!
Heaps of hits on YouTube!
Indev!
Spiders everywhere!
Check it out!
Holy cow, man!
It's a game!
Made in Sweden!
Uses LWJGL!
Reticulating splines!
Minecraft!
Yaaay!
413!
413!
413!
413!
Singleplayer!
Keyboard compatible!
Undocumented!
Ingots!
Exploding creepers!
That's no moon!
l33t!
Create!
Survive!
Dungeon!
I WARNED YOU ABOUT STAIRS BRO!!!!
I WARNED YOU ABOUT STAIRS BRO!!!!
I WARNED YOU ABOUT STAIRS BRO!!!!
Exclusive!
The bee's knees!
Closed source!
Classy!
Wow!
Not on steam!
Oh man!
Awesome community!
Pixels!
Teetsuuuuoooo!
Kaaneeeedaaaa!
Now with difficulty!
Enhanced!
90% bug free!
Pretty!
12 herbs and spices!
Fat free!
Absolutely no memes!
Free dental!
Cloud computing!
Hard to label!
Technically good!
Bringing home the bacon!
Indie!
GOTY!
Ceci n'est pas une title screen!
Euclidian!
Now in 3D!
Inspirational!
Herregud!
Complex cellular automata!
Yes, sir!
Played by cowboys!
Thousands of colors!
Try it!
Age of Wonders is better!
Try the mushroom stew!
Sensational!
Hot tamale, hot hot tamale!
Play him off, keyboard cat!
Guaranteed!
Alchemize ALL the things!
Alchemize ALL the things!
Macroscopic!
Bring it on!
Random splash!
Call your mother!
Monster infighting!
Loved by millions!
Ultimate edition!
Freaky!
Water proof!
Uninflammable!
Whoa, dude!
Roxy is best hacker!
Roxy is best hacker!
Roxy is best hacker!
All inclusive!
Tell your friends!
NP is not in P!
Notch <3 ez!
Music by C418!
Livestreamed!
Haunted!
Polynomial!
Terrestrial!
All is full of love!
Full of stars!
Scientific!
Cooler than Spock!
Collaborate and listen!
Never dig down!
Take frequent breaks!
Not linear!
Han shot first!
Nice to meet you!
Buckets of lava!
Ride the pig!
Larger than Earth!
sqrt(-1) love you!
Phobos anomaly!
Punching wood!
Falling off cliffs!
0% sugar!
150% hyperbole!
Synecdoche!
Let's danec!
Reference implementation!
Lewd with two dudes with food!
Kiss the sky!
20 GOTO 10!
Verlet intregration!
Peter Griffin!
Do not distribute!
Cogito ergo sum!
4815162342 lines of code!
A skeleton popped out!
The Work of Notch!
The sum of its parts!
BTAF used to be good!
I miss ADOM!
umop-apisdn!
OICU812!
Bring me Ray Cokes!
Finger-licking!
Thematic!
Pneumatic!
Sublime!
Octagonal!
Une baguette!
Gargamel plays it!
Rita is the new top dog!
SWM forever!
Representing Edsbyn!
Matt Damon!
Supercalifragilisticexpialidocious!
Consummate V's!
Cow Tools!
Double buffered!
Fan fiction!
Flaxkikare!
Jason! Jason! Jason!
Hotter than the sun!
Internet enabled!
Autonomous!
Engage!
Fantasy!
DRR! DRR! DRR!
Kick it root down!
Regional resources!
Woo, facepunch!
Woo, somethingawful!
Woo, /v/!
Woo, tigsource!
Woo, minecraftforum!
Woo, worldofminecraft!
Woo, reddit!
Woo, 2pp!
Google anlyticsed!
Now supports åäö!
Give us Gordon!
Tip your waiter!
Very fun!
12345 is a bad password!
Vote for net neutrality!
Lives in a pineapple under the sea!
MAP11 has two names!
Omnipotent!
Gasp!
...!
Bees, bees, bees, bees!
Jag känner en bot!
This text is hard to read if you play the game at the default resolution, but at 1080p it's fine!
Haha, LOL!
Hampsterdance!
Switches and ores!
Menger sponge!
idspispopd!
Eple (original edit)!
So fresh, so clean!
Slow acting portals!
Try the Nether!
Don't look directly at the bugs!
Oh, ok, Pigmen!
Finally with ladders!
Scary!
Play Minecraft, Watch Topgear, Get Pig!
Twittered about!
Jump up, jump up, and get down!
Joel is neat!
A riddle, wrapped in a mystery!
Huge tracts of land!
Welcome to your Doom!
Stay a while, stay forever!
Stay a while and listen!
Treatment for your rash!
"Autological" is!
Information wants to be free!
"Almost never" is an interesting concept!
Lots of truthiness!
The creeper is a spy!
Turing complete!
It's groundbreaking!
Let our battle's begin!
The sky is the limit!
Jeb has amazing hair!
Casual gaming!
Undefeated!
Kinda like Lemmings!
Follow the train, CJ!
Leveraging synergy!
This message will never appear on the splash screen, isn't that weird?
DungeonQuest is unfair!
110813!
90210!
Check out the far lands!
Tyrion would love it!
Also try VVVVVV!
Also try Super Meat Boy!
Also try Terraria!
Also try Mount And Blade!
Also try Project Zomboid!
Also try World of Goo!
Also try Limbo!
Also try Pixeljunk Shooter!
Also try Braid!
Also try Sburb!
Also try Sburb!
Also try Sburb!
That's super!
Bread is pain!
Read more books!
Khaaaaaaaaan!
Less addictive than TV Tropes!
More addictive than lemonade!
Bigger than a bread box!
Millions of peaches!
Fnord!
This is my true form!
Totally forgot about Dre!
Don't bother with the clones!
Pumpkinhead!
Hobo humping slobo babe!
Made by Jeb!
Has an ending!
Finally complete!
Feature packed!
Boots with the fur!
Stop, hammertime!
Testificates!
Conventional!
Homeomorphic to a 3-sphere!
Doesn't avoid double negatives!
Place ALL the blocks!
Does barrel rolls!
Meeting expectations!
PC gaming since 1873!
Ghoughpteighbteau tchoghs!
Déjà vu!
IT KEEPS HAPPENING!
IT KEEPS HAPPENING!
IT KEEPS HAPPENING!
I TOLD YOU DOG!
I TOLD YOU DOG!
Déjà vu!
Got your nose!
Afraid of the big, black bat!
Doesn't use the U-word!
Child's play!
See you next Friday or so!
From the streets of Södermalm!
150 bpm for 400000 minutes!
Technologic!
Funk soul brother!
Pumpa kungen!
Fetchstuck compatible!
Sylladex full!
Sylladex full!
Honk. HONK.
Honk. HONK.
You can't fight the Homestuck!
You can't fight the Homestuck!
==> Enter.
==> Enter.
STRIFE!
STRIFE!
8^y
8^y
Sweet Bro and Hella Jeff!
What pumpkin?
What pumpkin?
ABSCOND!
ABSCOND!
Check your Strife Specibus!
Ascend to God Tier!
Ascend to God Tier!
Got tiger!
Got tiger!
Motherfucking miracles!
Welcome to Skaia!
Welcome to Skaia!
Prospit or Derse?
Upgrading the Alchemiter!
Build Grist!
Build Grist!
Now with 100% more Faygo!
Sopor slime pie!
Grimdark!
Trickster mode engaged!
How HIGH do you even have to BE just to DO something like that........
Mayor of Can Town!
Spades Slick did nothing wrong!
It's hard and nobody understands.
Shenanigans!
The Scratch is happening!
612!
111111!
Jegus!
All the luck!
All the luck!
Dunkass!
Captchalogue this!
Captchalogue this!
Echeladder rung attained!
Megalovania originally played here!
Megalovania originally played here!
Music by Toby Fox!
Don't ask about the buckets!
Don't ask about the buckets!
Candy corn horns!
Your name is JOHN EGBERT.
Let me guess, your zodiac sign is a troll?
Put the bunny back in the box!
Do the windy thing!
Do the windy thing!
Betty Crocker is evil!
Betty Crocker is evil!
Beware the Batterwitch!
He is already here.
He is already here.
Doc Scratch is watching.
Con Air is a masterpiece!
Karkalicious!
SGRUB!
Sburb Beta!
Sburb Beta!
Knight of Time!
Seer of Light!
Heir of Breath!
Witch of Space!
Flipping the frog switch!
Forge the Bilious Sliver!
Midnight Crew!
We have the cue ball!
th1s 1s r3d1culous.
wweh...
glub glub...
DAVE: this is stupid
WHAT THE FUCK IS A CREEPER.
I AM GOING TO THROW A TANTRUM.
H3H3H3! 1 C4N SM3LL TH3 D14MOND 0R3!
T4ST3S L1K3 CH3RRY!
uHH, pLEASE DONT MINE THAT,,, iTS MINE.
ii jju2t want two play a game.
two much iinformatiion.
:33 < ac goes on a purrfect mining trip!
:33 < furrealsies!
I Cannot Believe You Just Mined That Dirt.
Please Stop Being Ridiculous.
All 8 of my 8lue diamonds! ::::::::)
You're w8ing for something?
D --> That block is insufficiently STRONG
D --> e%cuse me
tHaT sHiT iS mOtHeRfUcKiNg MiRaCuLoUs.
mY mOtHeRfUcKiNg BrOtHeR.
wwhats the point of minin anywway.
38) This is so GLUBBING exciting!
the dead p0pulati0n is b0thering me.
0k. i am t0tally fine with this.
ROXY: wonk ;)
ROXY: obfuscation!!!
ROXY: i am the best hacker
ROXY: meow
ROXY: dirk u there
ROXY: i am going to save u
ROXY: wtf is this block
ROXY: where r the fucking cats
ROXY: im kinda drunk
ROXY: the magic is real
ROXY: sweet catch!
ROXY: frigglish!
ROXY: where are all the wizards
ROXY: im putting on my god tier pjs
ROXY: pumpkins r legally outlawed
ROXY: mutieeeee
ROXY: cant wake up
ROXY: sup
ROXY: wonk ;)
JOHN: wow, okay.
JOHN: wow, okay.
JOHN: i am a true prankster!
JOHN: what are you even talking about?
JOHN: let's go do some ghostbusting!
JOHN: i'm not a homosexual!
JOHN: i am a true prankster!
Prankster's Gambit!
Prankster's Gambit!
Ectobiology!
Ectobiology!
JADE: hooray!!!
JADE: <3 <3 <3
JADE: beep beep meow
JADE: im going to build a tall tower!
JADE: hooray!!!
JADE: <3 <3 <3
Becquerel is a good boy!
Becquerel is a good boy!
Good dog. Best friend.
Good dog. Best friend.
Squiddleknit!
Squiddles!
DAVE: i am not a hero
DAVE: ironically of course
DAVE: time shenanigans
DAVE: lets drop it like its hot
DAVE: apple juice
DAVE: sweet catch
ROSE: Let the record state that I am not a fan of this.
ROSE: I will tear this game apart.
ROSE: The plot thickens.
ROSE: We are playing a game.
Pony Pals!
DIRK: You can't escape the miles.
DIRK: Sup.
DIRK: im going to build a robot.
Auto-Responder!
JAKE: Tally ho!
JAKE: Pip pip!
JAKE: By jove!
JAKE: Egads!
JAKE: Jolly good show
Jade Harley!!!
Jade Harley!!!
Jade Harley!!!
Jade Harley!!!
Jade Harley!!!

867
style.css

File diff suppressed because it is too large Load diff