mirror of
https://github.com/nixietab/picodulce.git
synced 2025-12-13 17:53:58 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c125f440f8 | ||
|
|
1543b36eb5 | ||
|
|
eb6b27ed23 | ||
|
|
0702f918dd | ||
|
|
5792bcb6eb | ||
|
|
b6241e7aad | ||
|
|
0ac8a1bf51 | ||
|
|
60e532b604 | ||
|
|
52b07aab71 | ||
|
|
bb48b3e293 | ||
|
|
f86bb54abe | ||
|
|
35e1cd7d17 | ||
|
|
c0ce394fda | ||
|
|
1f739f25e8 | ||
|
|
2d362843c2 | ||
|
|
ae14820c4d | ||
|
|
50b3f09ed1 | ||
|
|
b9bf71334b | ||
|
|
40bf8ea23e | ||
|
|
89cf8218f8 | ||
|
|
520a1b500f | ||
|
|
b6a3218bf6 | ||
|
|
c1c98aafae | ||
|
|
34c36c5b88 | ||
|
|
fc058913cd | ||
|
|
523431a557 | ||
|
|
72f7cbec9d | ||
|
|
fbdba505c9 | ||
|
|
479bc4486a |
64
.github/workflows/Build.yml
vendored
64
.github/workflows/Build.yml
vendored
@ -6,11 +6,9 @@ on:
|
|||||||
- version.json # Trigger on changes to version.json
|
- version.json # Trigger on changes to version.json
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
version-release:
|
run-on-version-change:
|
||||||
runs-on: windows-latest # Use Windows 10 runner
|
runs-on: windows-latest # Use Windows 10 runner
|
||||||
|
|
||||||
if: github.actor != 'github-actions[bot]' # Only run if the actor is not the GitHub Actions bot
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@ -26,44 +24,38 @@ jobs:
|
|||||||
pip install pyqt5 requests pywin32 pyinstaller pillow # Install specific dependencies
|
pip install pyqt5 requests pywin32 pyinstaller pillow # Install specific dependencies
|
||||||
|
|
||||||
- name: Create actions-temp folder
|
- name: Create actions-temp folder
|
||||||
run: mkdir actions-temp # Create the folder called actions-temp
|
run: |
|
||||||
|
mkdir actions-temp # Create the folder called actions-temp
|
||||||
|
|
||||||
- name: Download picoBuild.py script
|
- name: Download picoBuild.py script
|
||||||
run: curl -L -o actions-temp/picoBuild.py https://raw.githubusercontent.com/nixietab/picodulce-build-script/refs/heads/main/picoBuild.py
|
run: |
|
||||||
|
curl -L -o actions-temp/picoBuild.py https://raw.githubusercontent.com/nixietab/picodulce-build-script/refs/heads/main/picoBuild.py
|
||||||
|
|
||||||
- name: Run picoBuild.py script
|
- name: Run picoBuild.py script
|
||||||
run: python actions-temp/picoBuild.py
|
|
||||||
|
|
||||||
- name: Show directory structure
|
|
||||||
run: |
|
|
||||||
dir actions-temp
|
|
||||||
dir
|
|
||||||
|
|
||||||
- name: Get version and name from version.json
|
|
||||||
id: version_info
|
|
||||||
run: |
|
run: |
|
||||||
$versionJson = Get-Content version.json | ConvertFrom-Json
|
python actions-temp/picoBuild.py
|
||||||
echo "RELEASE_NAME=Release $($versionJson.version)" >> $env:GITHUB_ENV
|
|
||||||
echo "RELEASE_TAG=$($versionJson.version)" >> $env:GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Read version and name from version.json
|
||||||
id: create_release
|
id: version
|
||||||
uses: actions/create-release@v1
|
run: |
|
||||||
with:
|
$VERSION = (jq -r '.version' version.json)
|
||||||
tag_name: ${{ env.RELEASE_TAG }}
|
$NAME = (jq -r '.name' version.json)
|
||||||
release_name: ${{ env.RELEASE_NAME }}
|
echo "VERSION=$VERSION" >> $GITHUB_ENV # Store version in the environment variable
|
||||||
body: "This release was created automatically by a GitHub Action."
|
echo "NAME=$NAME" >> $GITHUB_ENV # Store name in the environment variable
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Upload Release Asset
|
- name: Tag release
|
||||||
uses: actions/upload-release-asset@v1
|
run: |
|
||||||
|
# Set Git user for the GitHub Actions runner
|
||||||
|
git config user.name "github-actions"
|
||||||
|
git config user.email "github-actions@github.com"
|
||||||
|
|
||||||
|
# Create the tag and push it to GitHub
|
||||||
|
$VERSION_NAME = "${env:VERSION} ${env:NAME}"
|
||||||
|
git tag -a "v${env:VERSION}" -m "Release $VERSION_NAME" # Tag the release with version number and name
|
||||||
|
git push origin "v${env:VERSION}" # Push the tag to GitHub
|
||||||
|
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
files: actions-temp/build/2hsu.exe # Specify the path to the file to upload
|
||||||
asset_path: build/2hsu.exe
|
|
||||||
asset_name: 2hsu.exe
|
|
||||||
asset_content_type: application/octet-stream
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|||||||
76
PKGBUILD
76
PKGBUILD
@ -1,76 +0,0 @@
|
|||||||
pkgname=picodulce
|
|
||||||
pkgver=0.13.5
|
|
||||||
pkgrel=1
|
|
||||||
pkgdesc="Launcher for Minecraft based on the zucaro library"
|
|
||||||
arch=('x86_64')
|
|
||||||
OPTIONS=(!strip !docs libtool emptydirs)
|
|
||||||
url="https://github.com/nixietab/picodulce"
|
|
||||||
license=('MIT')
|
|
||||||
depends=('python' 'python-virtualenv' 'xdg-utils')
|
|
||||||
makedepends=('git')
|
|
||||||
source=("git+https://github.com/nixietab/picodulce.git")
|
|
||||||
sha256sums=('SKIP')
|
|
||||||
|
|
||||||
package() {
|
|
||||||
cd "$srcdir/$pkgname"
|
|
||||||
|
|
||||||
# Create a directory for the application in the user's home directory
|
|
||||||
install -dm755 "$pkgdir/usr/share/$pkgname"
|
|
||||||
|
|
||||||
# Copy all project files to the created directory
|
|
||||||
cp -r . "$pkgdir/usr/share/$pkgname"
|
|
||||||
|
|
||||||
# Create a virtual environment
|
|
||||||
python -m venv "$pkgdir/usr/share/$pkgname/venv"
|
|
||||||
|
|
||||||
# Activate the virtual environment and install dependencies
|
|
||||||
source "$pkgdir/usr/share/$pkgname/venv/bin/activate"
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# Create a run.sh script
|
|
||||||
install -Dm755 /dev/stdin "$pkgdir/usr/share/$pkgname/run.sh" <<EOF
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
if [ ! -d "venv" ]; then
|
|
||||||
echo "venv folder does not exist. Creating virtual environment..."
|
|
||||||
python3 -m venv venv
|
|
||||||
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
echo "Installing required packages..."
|
|
||||||
pip install -r requirements.txt
|
|
||||||
else
|
|
||||||
source venv/bin/activate
|
|
||||||
fi
|
|
||||||
|
|
||||||
python picodulce.py
|
|
||||||
EOF
|
|
||||||
|
|
||||||
|
|
||||||
# Make the run.sh script executable
|
|
||||||
chmod +x "$pkgdir/usr/share/$pkgname/run.sh"
|
|
||||||
|
|
||||||
# Create a desktop entry for the application
|
|
||||||
install -Dm644 /dev/stdin "$pkgdir/usr/share/applications/$pkgname.desktop" <<EOF
|
|
||||||
[Desktop Entry]
|
|
||||||
Name=Picodulce
|
|
||||||
Exec=/usr/share/picodulce/run.sh
|
|
||||||
Icon=/usr/share/picodulce/launcher_icon.ico
|
|
||||||
Terminal=true
|
|
||||||
Type=Application
|
|
||||||
Comment=Picodulce Launcher
|
|
||||||
Categories=Game;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Ensure the normal user has permission to write to the picodulce folder
|
|
||||||
chown -R "$USER:$USER" "$pkgdir/usr/share/$pkgname"
|
|
||||||
chmod -R u+w "$pkgdir/usr/share/$pkgname"
|
|
||||||
|
|
||||||
#Install into bin
|
|
||||||
install -Dm755 /dev/stdin "$pkgdir/usr/bin/picodulce" <<EOF
|
|
||||||
#!/bin/bash
|
|
||||||
cd /usr/share/picodulce/
|
|
||||||
exec ./run.sh
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
# vim:set ts=2 sw=2 et:
|
|
||||||
31
README.md
31
README.md
@ -27,7 +27,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
Picodulce is a feature-rich launcher for Minecraft, developed using Qt5. It serves as a graphical user interface (GUI) for the [zucaro backend](https://github.com/nixietab/zucaro), providing users with a seamless experience in managing and launching game versions.
|
Picodulce is a feature-rich launcher for Minecraft, developed using Qt5. It serves as a graphical user interface (GUI) for the picomc project, providing users with a seamless experience in managing and launching game versions.
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
@ -38,37 +38,16 @@
|
|||||||
- **Offline and Online Support**: Whether you're connected to Microsoft or not, Picodulce ensures you can still enjoy your game by supporting both offline and online modes.
|
- **Offline and Online Support**: Whether you're connected to Microsoft or not, Picodulce ensures you can still enjoy your game by supporting both offline and online modes.
|
||||||
- **Integrated Mod Manager**: Includes the [Marroc Mod Manager](https://github.com/nixietab/marroc), enabling users to effortlessly manage and customize their game with mods and texturepacks.
|
- **Integrated Mod Manager**: Includes the [Marroc Mod Manager](https://github.com/nixietab/marroc), enabling users to effortlessly manage and customize their game with mods and texturepacks.
|
||||||
- **Custom Theme Support**: Create and apply personalized themes with ease. A dedicated repository and guide are [available to help you get started.](https://github.com/nixietab/picodulce-themes)
|
- **Custom Theme Support**: Create and apply personalized themes with ease. A dedicated repository and guide are [available to help you get started.](https://github.com/nixietab/picodulce-themes)
|
||||||
|
-- **Instance Support**: Allows users to create and manage multiple game instances, each with its own configuration, mods, and settings
|
||||||
# Installation
|
# Installation
|
||||||
|
If you are on windows you may be more interested in a [installer](https://github.com/nixietab/2hsu/releases/download/release/2hsu.exe)
|
||||||
## Windows
|
|
||||||
For Windows systems using the [installer](https://github.com/nixietab/picodulce/releases/latest) is recommended
|
|
||||||
|
|
||||||
# Linux (Generic)
|
|
||||||
We have a install script, to use it run:
|
|
||||||
|
|
||||||
~~~
|
|
||||||
curl -sSL https://raw.githubusercontent.com/nixietab/picodulce/refs/heads/main/install-universal.sh | bash
|
|
||||||
~~~
|
|
||||||
|
|
||||||
## Arch Linux
|
|
||||||
The package is available in the [AUR](https://aur.archlinux.org/packages/picodulce) as ```picodulce```
|
|
||||||
|
|
||||||
For installing on Arch without using an AUR helper a PKGBUILD is provided
|
|
||||||
```
|
|
||||||
git clone https://github.com/nixietab/picodulce.git
|
|
||||||
cd picodulce
|
|
||||||
makepkg -si
|
|
||||||
```
|
|
||||||
|
|
||||||
## Other OS
|
|
||||||
|
|
||||||
### 1. Clone the repository
|
### 1. Clone the repository
|
||||||
|
|
||||||
``` git clone https://github.com/nixietab/picodulce ```
|
``` git clone https://github.com/nixietab/picodulce ```
|
||||||
|
|
||||||
### 2. (Optional) Set Up a Virtual Environment
|
### 2. (Optional) Set Up a Virtual Environment
|
||||||
Setting up a virtual environment is recommended to avoid dependency conflicts. Picodulce relies on the path of the `zucaro` project, and using a virtual environment helps prevent errors.
|
Setting up a virtual environment is recommended to avoid dependency conflicts. Picodulce relies on the path of the `picomc` project, and using a virtual environment helps prevent errors.
|
||||||
|
|
||||||
Create the virtual environment:
|
Create the virtual environment:
|
||||||
|
|
||||||
@ -93,7 +72,7 @@ On the venv run it as a normal python script
|
|||||||
|
|
||||||
```python picodulce.py```
|
```python picodulce.py```
|
||||||
|
|
||||||
Just make sure you have Java installed for running the actual game, or check the "manage java" option inside the launcher settings
|
Just make sure you have Java installed for running the actual game
|
||||||
|
|
||||||
### About the name
|
### About the name
|
||||||
The name "Picodulce" comes from a popular Argentinian candy. This reflects the enjoyable and user-friendly experience that the launcher aims to provide, making game management straightforward and pleasant.
|
The name "Picodulce" comes from a popular Argentinian candy. This reflects the enjoyable and user-friendly experience that the launcher aims to provide, making game management straightforward and pleasant.
|
||||||
|
|||||||
390
authser.py
390
authser.py
@ -1,390 +0,0 @@
|
|||||||
import sys
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from pathlib import Path
|
|
||||||
from PyQt5.QtWidgets import (QApplication, QDialog, QLabel, QVBoxLayout,
|
|
||||||
QPushButton, QLineEdit, QMessageBox)
|
|
||||||
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QUrl, QObject
|
|
||||||
from PyQt5.QtGui import QDesktopServices
|
|
||||||
from zucaro.logging import logger
|
|
||||||
from zucaro.launcher import get_default_root, Launcher
|
|
||||||
|
|
||||||
# Constants for Microsoft Authentication
|
|
||||||
URL_DEVICE_AUTH = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"
|
|
||||||
URL_TOKEN = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
|
|
||||||
URL_XBL = "https://user.auth.xboxlive.com/user/authenticate"
|
|
||||||
URL_XSTS = "https://xsts.auth.xboxlive.com/xsts/authorize"
|
|
||||||
URL_MC = "https://api.minecraftservices.com/authentication/login_with_xbox"
|
|
||||||
URL_PROFILE = "https://api.minecraftservices.com/minecraft/profile"
|
|
||||||
|
|
||||||
CLIENT_ID = "c52aed44-3b4d-4215-99c5-824033d2bc0f"
|
|
||||||
SCOPE = "XboxLive.signin offline_access"
|
|
||||||
GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"
|
|
||||||
|
|
||||||
class AuthDialog(QDialog):
|
|
||||||
def __init__(self, url, code, parent=None, error_mode=False):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setWindowTitle("Microsoft Authentication")
|
|
||||||
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
|
|
||||||
self.setModal(True)
|
|
||||||
self.setup_ui(url, code, error_mode)
|
|
||||||
|
|
||||||
def setup_ui(self, url, code, error_mode):
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
|
|
||||||
if error_mode:
|
|
||||||
error_label = QLabel("Error in Login - Please try again")
|
|
||||||
error_label.setStyleSheet("QLabel { color: red; font-weight: bold; }")
|
|
||||||
layout.addWidget(error_label)
|
|
||||||
|
|
||||||
instructions = QLabel(
|
|
||||||
"To authenticate your Microsoft Account:\n\n"
|
|
||||||
"1. Click 'Open Authentication Page' or visit:\n"
|
|
||||||
"2. Copy the code below\n"
|
|
||||||
"3. Paste the code on the Microsoft website\n"
|
|
||||||
"4. After completing authentication, click 'I've Completed Authentication'"
|
|
||||||
)
|
|
||||||
instructions.setWordWrap(True)
|
|
||||||
layout.addWidget(instructions)
|
|
||||||
|
|
||||||
url_label = QLabel(url)
|
|
||||||
url_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
|
||||||
url_label.setWordWrap(True)
|
|
||||||
layout.addWidget(url_label)
|
|
||||||
|
|
||||||
self.code_input = QLineEdit(code)
|
|
||||||
self.code_input.setReadOnly(True)
|
|
||||||
self.code_input.setAlignment(Qt.AlignCenter)
|
|
||||||
self.code_input.setStyleSheet("""
|
|
||||||
QLineEdit {
|
|
||||||
font-size: 16pt;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
layout.addWidget(self.code_input)
|
|
||||||
|
|
||||||
copy_button = QPushButton("Copy Code")
|
|
||||||
copy_button.clicked.connect(self.copy_code)
|
|
||||||
layout.addWidget(copy_button)
|
|
||||||
|
|
||||||
open_url_button = QPushButton("Open Authentication Page")
|
|
||||||
open_url_button.clicked.connect(lambda: self.open_url(url))
|
|
||||||
layout.addWidget(open_url_button)
|
|
||||||
|
|
||||||
continue_button = QPushButton("I've Completed Authentication")
|
|
||||||
continue_button.clicked.connect(self.accept)
|
|
||||||
layout.addWidget(continue_button)
|
|
||||||
|
|
||||||
def copy_code(self):
|
|
||||||
clipboard = QApplication.clipboard()
|
|
||||||
clipboard.setText(self.code_input.text())
|
|
||||||
|
|
||||||
def open_url(self, url):
|
|
||||||
QDesktopServices.openUrl(QUrl(url))
|
|
||||||
|
|
||||||
class AuthenticationThread(QThread):
|
|
||||||
auth_data_received = pyqtSignal(dict)
|
|
||||||
error_occurred = pyqtSignal(str)
|
|
||||||
auth_error_detected = pyqtSignal(str)
|
|
||||||
finished = pyqtSignal()
|
|
||||||
access_token_received = pyqtSignal(dict)
|
|
||||||
|
|
||||||
def __init__(self, username):
|
|
||||||
super().__init__()
|
|
||||||
self.username = username
|
|
||||||
self.device_code = None
|
|
||||||
self.is_running = True
|
|
||||||
|
|
||||||
async def _ms_oauth(self):
|
|
||||||
data = {"client_id": CLIENT_ID, "scope": SCOPE}
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.post(URL_DEVICE_AUTH, data=data) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
raise Exception(f"Failed to get device code: {await resp.text()}")
|
|
||||||
j = await resp.json()
|
|
||||||
self.device_code = j["device_code"]
|
|
||||||
self.auth_data_received.emit({
|
|
||||||
'url': j["verification_uri"],
|
|
||||||
'code': j["user_code"]
|
|
||||||
})
|
|
||||||
|
|
||||||
while self.is_running:
|
|
||||||
data = {
|
|
||||||
"grant_type": GRANT_TYPE,
|
|
||||||
"client_id": CLIENT_ID,
|
|
||||||
"device_code": self.device_code
|
|
||||||
}
|
|
||||||
|
|
||||||
async with session.post(URL_TOKEN, data=data) as resp:
|
|
||||||
j = await resp.json()
|
|
||||||
if resp.status == 400:
|
|
||||||
if j["error"] == "authorization_pending":
|
|
||||||
await asyncio.sleep(2)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
raise Exception(j["error_description"])
|
|
||||||
elif resp.status != 200:
|
|
||||||
raise Exception(f"Token request failed: {j}")
|
|
||||||
|
|
||||||
return j["access_token"], j["refresh_token"]
|
|
||||||
|
|
||||||
raise Exception("Authentication cancelled by user")
|
|
||||||
|
|
||||||
async def _xbl_auth(self, access_token):
|
|
||||||
data = {
|
|
||||||
"Properties": {
|
|
||||||
"AuthMethod": "RPS",
|
|
||||||
"SiteName": "user.auth.xboxlive.com",
|
|
||||||
"RpsTicket": f"d={access_token}"
|
|
||||||
},
|
|
||||||
"RelyingParty": "http://auth.xboxlive.com",
|
|
||||||
"TokenType": "JWT"
|
|
||||||
}
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.post(URL_XBL, json=data) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
raise Exception(f"XBL auth failed: {await resp.text()}")
|
|
||||||
j = await resp.json()
|
|
||||||
return j["Token"], j["DisplayClaims"]["xui"][0]["uhs"]
|
|
||||||
|
|
||||||
async def _xsts_auth(self, xbl_token):
|
|
||||||
data = {
|
|
||||||
"Properties": {
|
|
||||||
"SandboxId": "RETAIL",
|
|
||||||
"UserTokens": [xbl_token]
|
|
||||||
},
|
|
||||||
"RelyingParty": "rp://api.minecraftservices.com/",
|
|
||||||
"TokenType": "JWT"
|
|
||||||
}
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.post(URL_XSTS, json=data) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
raise Exception(f"XSTS auth failed: {await resp.text()}")
|
|
||||||
j = await resp.json()
|
|
||||||
return j["Token"]
|
|
||||||
|
|
||||||
async def _mc_auth(self, uhs, xsts_token):
|
|
||||||
data = {
|
|
||||||
"identityToken": f"XBL3.0 x={uhs};{xsts_token}"
|
|
||||||
}
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.post(URL_MC, json=data) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
raise Exception(f"MC auth failed: {await resp.text()}")
|
|
||||||
j = await resp.json()
|
|
||||||
return j["access_token"]
|
|
||||||
|
|
||||||
async def _get_profile(self, mc_token):
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {mc_token}"
|
|
||||||
}
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get(URL_PROFILE, headers=headers) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
raise Exception(f"Profile request failed: {await resp.text()}")
|
|
||||||
return await resp.json()
|
|
||||||
|
|
||||||
async def _auth_flow(self):
|
|
||||||
try:
|
|
||||||
ms_access_token, refresh_token = await self._ms_oauth()
|
|
||||||
xbl_token, uhs = await self._xbl_auth(ms_access_token)
|
|
||||||
xsts_token = await self._xsts_auth(xbl_token)
|
|
||||||
mc_token = await self._mc_auth(uhs, xsts_token)
|
|
||||||
profile = await self._get_profile(mc_token)
|
|
||||||
|
|
||||||
self.access_token_received.emit({
|
|
||||||
'access_token': mc_token,
|
|
||||||
'refresh_token': refresh_token,
|
|
||||||
'profile': profile
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.error_occurred.emit(str(e))
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
try:
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
loop.run_until_complete(self._auth_flow())
|
|
||||||
except Exception as e:
|
|
||||||
self.error_occurred.emit(str(e))
|
|
||||||
finally:
|
|
||||||
self.finished.emit()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.is_running = False
|
|
||||||
|
|
||||||
class MinecraftAuthenticator(QObject):
|
|
||||||
auth_finished = pyqtSignal(bool, str)
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.auth_thread = None
|
|
||||||
self.auth_dialog = None
|
|
||||||
self.success = False
|
|
||||||
self.error_message = None
|
|
||||||
self.username = None
|
|
||||||
|
|
||||||
# Initialize the launcher to get the correct config path
|
|
||||||
with Launcher.new() as launcher:
|
|
||||||
self.config_path = launcher.root
|
|
||||||
|
|
||||||
def authenticate(self, username):
|
|
||||||
self.username = username
|
|
||||||
self.success = False
|
|
||||||
|
|
||||||
# Create accounts.json if it doesn't exist
|
|
||||||
if not self.save_to_accounts_json():
|
|
||||||
self.auth_finished.emit(False, self.error_message)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check if account is online
|
|
||||||
if not self.validate_account_type():
|
|
||||||
self.auth_finished.emit(False, "Cannot authenticate an offline account")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.auth_thread = AuthenticationThread(username)
|
|
||||||
self.auth_thread.auth_data_received.connect(self.show_auth_dialog)
|
|
||||||
self.auth_thread.error_occurred.connect(self.show_error)
|
|
||||||
self.auth_thread.access_token_received.connect(self.on_access_token_received)
|
|
||||||
self.auth_thread.finished.connect(self.on_authentication_finished)
|
|
||||||
self.auth_thread.start()
|
|
||||||
|
|
||||||
def show_auth_dialog(self, auth_data):
|
|
||||||
if self.auth_dialog is not None:
|
|
||||||
self.auth_dialog.close()
|
|
||||||
|
|
||||||
self.auth_dialog = AuthDialog(auth_data['url'], auth_data['code'])
|
|
||||||
|
|
||||||
result = self.auth_dialog.exec_()
|
|
||||||
|
|
||||||
if result != QDialog.Accepted:
|
|
||||||
self.auth_thread.stop()
|
|
||||||
|
|
||||||
def show_error(self, error_msg):
|
|
||||||
self.error_message = error_msg
|
|
||||||
self.success = False
|
|
||||||
|
|
||||||
def validate_account_type(self):
|
|
||||||
try:
|
|
||||||
accounts_file = Path(self.config_path) / "accounts.json"
|
|
||||||
if accounts_file.exists():
|
|
||||||
with open(accounts_file) as f:
|
|
||||||
config = json.load(f)
|
|
||||||
|
|
||||||
if self.username in config["accounts"]:
|
|
||||||
return config["accounts"][self.username].get("microsoft", False)
|
|
||||||
|
|
||||||
return True # New account, will be created as microsoft
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to validate account type: {str(e)}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def save_to_accounts_json(self):
|
|
||||||
try:
|
|
||||||
accounts_file = Path(self.config_path) / "accounts.json"
|
|
||||||
|
|
||||||
if accounts_file.exists():
|
|
||||||
with open(accounts_file) as f:
|
|
||||||
config = json.load(f)
|
|
||||||
else:
|
|
||||||
config = {
|
|
||||||
"default": None,
|
|
||||||
"accounts": {},
|
|
||||||
"client_token": str(uuid.uuid4())
|
|
||||||
}
|
|
||||||
accounts_file.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Only create/update if account doesn't exist
|
|
||||||
if self.username not in config["accounts"]:
|
|
||||||
config["accounts"][self.username] = {
|
|
||||||
"uuid": "-",
|
|
||||||
"online": True,
|
|
||||||
"microsoft": True,
|
|
||||||
"gname": "-",
|
|
||||||
"access_token": "-",
|
|
||||||
"refresh_token": "-",
|
|
||||||
"is_authenticated": False
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set as default if no default exists
|
|
||||||
if config["default"] is None:
|
|
||||||
config["default"] = self.username
|
|
||||||
|
|
||||||
with open(accounts_file, 'w') as f:
|
|
||||||
json.dump(config, f, indent=4)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to initialize account data: {str(e)}")
|
|
||||||
self.error_message = f"Failed to initialize account data: {str(e)}"
|
|
||||||
return False
|
|
||||||
|
|
||||||
def on_access_token_received(self, data):
|
|
||||||
try:
|
|
||||||
accounts_file = Path(self.config_path) / "accounts.json"
|
|
||||||
|
|
||||||
with open(accounts_file) as f:
|
|
||||||
config = json.load(f)
|
|
||||||
|
|
||||||
if self.username in config["accounts"]:
|
|
||||||
config["accounts"][self.username].update({
|
|
||||||
"access_token": data['access_token'],
|
|
||||||
"refresh_token": data['refresh_token'],
|
|
||||||
"uuid": data['profile']['id'],
|
|
||||||
"gname": data['profile']['name'],
|
|
||||||
"is_authenticated": True
|
|
||||||
})
|
|
||||||
|
|
||||||
with open(accounts_file, 'w') as f:
|
|
||||||
json.dump(config, f, indent=4)
|
|
||||||
|
|
||||||
self.success = True
|
|
||||||
else:
|
|
||||||
raise Exception("Account not found in configuration")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to update account data: {str(e)}")
|
|
||||||
self.error_message = f"Failed to update account data: {str(e)}"
|
|
||||||
self.success = False
|
|
||||||
|
|
||||||
# We don't emit here, we wait for the thread to finish
|
|
||||||
|
|
||||||
def on_authentication_finished(self):
|
|
||||||
if self.auth_dialog is not None:
|
|
||||||
self.auth_dialog.close()
|
|
||||||
self.auth_dialog = None
|
|
||||||
|
|
||||||
if self.auth_thread:
|
|
||||||
self.auth_thread.stop()
|
|
||||||
self.auth_thread = None
|
|
||||||
|
|
||||||
if not self.success:
|
|
||||||
self.auth_finished.emit(False, self.error_message)
|
|
||||||
else:
|
|
||||||
self.auth_finished.emit(True, f"Successfully authenticated account: {self.username}")
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
if self.auth_dialog is not None:
|
|
||||||
self.auth_dialog.close()
|
|
||||||
self.auth_dialog = None
|
|
||||||
|
|
||||||
if self.auth_thread and self.auth_thread.isRunning():
|
|
||||||
self.auth_thread.stop()
|
|
||||||
self.auth_thread.wait()
|
|
||||||
|
|
||||||
def create_authenticator():
|
|
||||||
"""Factory function to create a new MinecraftAuthenticator instance"""
|
|
||||||
return MinecraftAuthenticator()
|
|
||||||
231
healthcheck.py
231
healthcheck.py
@ -1,231 +0,0 @@
|
|||||||
import os
|
|
||||||
import json
|
|
||||||
import shutil
|
|
||||||
import modulecli
|
|
||||||
from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QLabel, QProgressBar
|
|
||||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
|
||||||
import sys
|
|
||||||
|
|
||||||
class CopyThread(QThread):
|
|
||||||
progress_changed = pyqtSignal(int)
|
|
||||||
finished = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, src_dir, dst_dir):
|
|
||||||
super().__init__()
|
|
||||||
self.src_dir = src_dir
|
|
||||||
self.dst_dir = dst_dir
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
# Gather all files recursively
|
|
||||||
files_to_copy = []
|
|
||||||
for root, dirs, files in os.walk(self.src_dir):
|
|
||||||
for f in files:
|
|
||||||
full_path = os.path.join(root, f)
|
|
||||||
relative_path = os.path.relpath(full_path, self.src_dir)
|
|
||||||
files_to_copy.append(relative_path)
|
|
||||||
|
|
||||||
total_files = len(files_to_copy)
|
|
||||||
copied_files = 0
|
|
||||||
|
|
||||||
for relative_path in files_to_copy:
|
|
||||||
src_path = os.path.join(self.src_dir, relative_path)
|
|
||||||
dst_path = os.path.join(self.dst_dir, relative_path)
|
|
||||||
dst_folder = os.path.dirname(dst_path)
|
|
||||||
|
|
||||||
if not os.path.exists(dst_folder):
|
|
||||||
try:
|
|
||||||
os.makedirs(dst_folder)
|
|
||||||
except PermissionError:
|
|
||||||
print(f"Skipping folder {dst_folder} (permission denied)")
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
shutil.copy2(src_path, dst_path)
|
|
||||||
except PermissionError:
|
|
||||||
print(f"Skipping file {dst_path} (permission denied)")
|
|
||||||
|
|
||||||
copied_files += 1
|
|
||||||
progress_percent = int((copied_files / total_files) * 100)
|
|
||||||
self.progress_changed.emit(progress_percent)
|
|
||||||
|
|
||||||
self.finished.emit()
|
|
||||||
|
|
||||||
|
|
||||||
class HealthCheck:
|
|
||||||
def __init__(self):
|
|
||||||
self.config = None
|
|
||||||
|
|
||||||
def check_config_file(self):
|
|
||||||
config_path = "config.json"
|
|
||||||
default_config = {
|
|
||||||
"IsRCPenabled": False,
|
|
||||||
"CheckUpdate": False,
|
|
||||||
"IsBleeding": False,
|
|
||||||
"LastPlayed": "",
|
|
||||||
"TotalPlaytime": 0,
|
|
||||||
"IsFirstLaunch": True,
|
|
||||||
"Instance": "default",
|
|
||||||
"Theme": "Dark.json",
|
|
||||||
"ThemeBackground": True,
|
|
||||||
"ThemeRepository": "https://raw.githubusercontent.com/nixietab/picodulce-themes/main/repo.json",
|
|
||||||
"Locale": "en",
|
|
||||||
"ManageJava": False,
|
|
||||||
"MaxRAM": "2G",
|
|
||||||
"JavaPath": "",
|
|
||||||
"ZucaroCheck": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
if not os.path.exists(config_path):
|
|
||||||
with open(config_path, "w") as config_file:
|
|
||||||
json.dump(default_config, config_file, indent=4)
|
|
||||||
self.config = default_config
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(config_path, "r") as config_file:
|
|
||||||
self.config = json.load(config_file)
|
|
||||||
except (json.JSONDecodeError, ValueError):
|
|
||||||
with open(config_path, "w") as config_file:
|
|
||||||
json.dump(default_config, config_file, indent=4)
|
|
||||||
self.config = default_config
|
|
||||||
return
|
|
||||||
|
|
||||||
updated = False
|
|
||||||
for key, value in default_config.items():
|
|
||||||
if key not in self.config:
|
|
||||||
self.config[key] = value
|
|
||||||
updated = True
|
|
||||||
|
|
||||||
if updated:
|
|
||||||
with open(config_path, "w") as config_file:
|
|
||||||
json.dump(self.config, config_file, indent=4)
|
|
||||||
|
|
||||||
def get_folder_size(self, folder_path):
|
|
||||||
total_size = 0
|
|
||||||
for dirpath, dirnames, filenames in os.walk(folder_path):
|
|
||||||
for f in filenames:
|
|
||||||
fp = os.path.join(dirpath, f)
|
|
||||||
if os.path.isfile(fp):
|
|
||||||
total_size += os.path.getsize(fp)
|
|
||||||
return total_size
|
|
||||||
|
|
||||||
def zucaro_health_check(self):
|
|
||||||
if self.config.get("ZucaroCheck"):
|
|
||||||
return
|
|
||||||
|
|
||||||
output = modulecli.run_command("instance dir").strip()
|
|
||||||
instance_dir = os.path.abspath(output)
|
|
||||||
base_dir = os.path.abspath(os.path.join(instance_dir, "..", ".."))
|
|
||||||
|
|
||||||
possible_zucaro = [os.path.join(base_dir, "zucaro"), os.path.join(base_dir, ".zucaro")]
|
|
||||||
possible_picomc = [os.path.join(base_dir, "picomc"), os.path.join(base_dir, ".picomc")]
|
|
||||||
|
|
||||||
zucaro_dir = next((d for d in possible_zucaro if os.path.exists(d)), None)
|
|
||||||
picomc_dir = next((d for d in possible_picomc if os.path.exists(d)), None)
|
|
||||||
|
|
||||||
if picomc_dir is None or zucaro_dir is None:
|
|
||||||
print("Required directories not found. Skipping copy.")
|
|
||||||
# Mark the check as done so it wont run again
|
|
||||||
self.config["ZucaroCheck"] = True
|
|
||||||
with open("config.json", "w") as f:
|
|
||||||
json.dump(self.config, f, indent=4)
|
|
||||||
return
|
|
||||||
|
|
||||||
picomc_size = self.get_folder_size(picomc_dir)
|
|
||||||
zucaro_size = self.get_folder_size(zucaro_dir)
|
|
||||||
|
|
||||||
if picomc_size <= zucaro_size:
|
|
||||||
print("No action needed. Zucaro folder is not smaller than Picomc.")
|
|
||||||
# Update config so the check is considered done
|
|
||||||
self.config["ZucaroCheck"] = True
|
|
||||||
with open("config.json", "w") as f:
|
|
||||||
json.dump(self.config, f, indent=4)
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"Copying Picomc ({picomc_size} bytes) to Zucaro ({zucaro_size} bytes)...")
|
|
||||||
|
|
||||||
app = QApplication.instance() or QApplication(sys.argv)
|
|
||||||
dialog = QDialog()
|
|
||||||
dialog.setWindowTitle("Working...")
|
|
||||||
dialog.setWindowModality(Qt.ApplicationModal)
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
label = QLabel("Working on stuff, please wait...")
|
|
||||||
progress = QProgressBar()
|
|
||||||
progress.setValue(0)
|
|
||||||
layout.addWidget(label)
|
|
||||||
layout.addWidget(progress)
|
|
||||||
dialog.setLayout(layout)
|
|
||||||
|
|
||||||
# Setup copy thread
|
|
||||||
thread = CopyThread(picomc_dir, zucaro_dir)
|
|
||||||
thread.progress_changed.connect(progress.setValue)
|
|
||||||
thread.finished.connect(dialog.accept)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
dialog.exec_() # Runs the modal event loop
|
|
||||||
|
|
||||||
# Mark as done
|
|
||||||
self.config["ZucaroCheck"] = True
|
|
||||||
with open("config.json", "w") as f:
|
|
||||||
json.dump(self.config, f, indent=4)
|
|
||||||
|
|
||||||
print("Copy completed.")
|
|
||||||
|
|
||||||
def themes_integrity(self):
|
|
||||||
themes_folder = "themes"
|
|
||||||
dark_theme_file = os.path.join(themes_folder, "Dark.json")
|
|
||||||
native_theme_file = os.path.join(themes_folder, "Native.json")
|
|
||||||
|
|
||||||
dark_theme_content = {
|
|
||||||
"manifest": {
|
|
||||||
"name": "Dark",
|
|
||||||
"description": "The default picodulce launcher theme",
|
|
||||||
"author": "Nixietab",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"palette": {
|
|
||||||
"Window": "#353535",
|
|
||||||
"WindowText": "#ffffff",
|
|
||||||
"Base": "#191919",
|
|
||||||
"AlternateBase": "#353535",
|
|
||||||
"ToolTipBase": "#ffffff",
|
|
||||||
"ToolTipText": "#ffffff",
|
|
||||||
"Text": "#ffffff",
|
|
||||||
"Button": "#353535",
|
|
||||||
"ButtonText": "#ffffff",
|
|
||||||
"BrightText": "#ff0000",
|
|
||||||
"Link": "#2a82da",
|
|
||||||
"Highlight": "#4bb679",
|
|
||||||
"HighlightedText": "#ffffff"
|
|
||||||
},
|
|
||||||
"background_image_base64": ""
|
|
||||||
}
|
|
||||||
|
|
||||||
native_theme_content = {
|
|
||||||
"manifest": {
|
|
||||||
"name": "Native",
|
|
||||||
"description": "The native looks of your OS",
|
|
||||||
"author": "Your Qt Style",
|
|
||||||
"license": "Any"
|
|
||||||
},
|
|
||||||
"palette": {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if not os.path.exists(themes_folder):
|
|
||||||
print(f"Creating folder: {themes_folder}")
|
|
||||||
os.makedirs(themes_folder)
|
|
||||||
|
|
||||||
if not os.path.isfile(dark_theme_file):
|
|
||||||
print(f"Creating file: {dark_theme_file}")
|
|
||||||
with open(dark_theme_file, "w", encoding="utf-8") as file:
|
|
||||||
json.dump(dark_theme_content, file, indent=2)
|
|
||||||
print("Dark.json has been created successfully.")
|
|
||||||
|
|
||||||
if not os.path.isfile(native_theme_file):
|
|
||||||
print(f"Creating file: {native_theme_file}")
|
|
||||||
with open(native_theme_file, "w", encoding="utf-8") as file:
|
|
||||||
json.dump(native_theme_content, file, indent=2)
|
|
||||||
print("Native.json has been created successfully.")
|
|
||||||
|
|
||||||
if os.path.isfile(dark_theme_file) and os.path.isfile(native_theme_file):
|
|
||||||
print("Theme Integrity OK")
|
|
||||||
BIN
holiday.ico
BIN
holiday.ico
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 108 KiB |
@ -1,110 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
PICODULCE_DIR="$HOME/.picodulce"
|
|
||||||
GIT_URL="https://github.com/nixietab/picodulce.git"
|
|
||||||
DESKTOP_FILE="$HOME/.local/share/applications/picodulce.desktop"
|
|
||||||
BIN_FILE="/usr/bin/picodulce"
|
|
||||||
|
|
||||||
# --- Helper functions ---
|
|
||||||
msg() {
|
|
||||||
echo -e "\033[1;32m$1\033[0m"
|
|
||||||
}
|
|
||||||
|
|
||||||
err() {
|
|
||||||
echo -e "\033[1;31m$1\033[0m" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
pause() {
|
|
||||||
read -rp "Press Enter to continue..."
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Check dependencies ---
|
|
||||||
msg "Checking Python3..."
|
|
||||||
if ! command -v python3 >/dev/null; then
|
|
||||||
err "Python3 is not installed. Please install it first."
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg "Checking venv module..."
|
|
||||||
if ! python3 -m venv --help >/dev/null 2>&1; then
|
|
||||||
err "python3-venv is not available. Please install it."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Clone repo ---
|
|
||||||
msg "Cloning Picodulce repo..."
|
|
||||||
rm -rf "$PICODULCE_DIR"
|
|
||||||
git clone "$GIT_URL" "$PICODULCE_DIR"
|
|
||||||
|
|
||||||
# --- Create virtual environment ---
|
|
||||||
cd "$PICODULCE_DIR"
|
|
||||||
msg "Creating virtual environment..."
|
|
||||||
python3 -m venv venv
|
|
||||||
source venv/bin/activate
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# --- Create run.sh ---
|
|
||||||
msg "Creating run.sh..."
|
|
||||||
cat > "$PICODULCE_DIR/run.sh" <<'EOF'
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
if [ ! -d "venv" ]; then
|
|
||||||
echo "venv folder does not exist. Creating virtual environment..."
|
|
||||||
python3 -m venv venv
|
|
||||||
source venv/bin/activate
|
|
||||||
echo "Installing required packages..."
|
|
||||||
pip install -r requirements.txt
|
|
||||||
else
|
|
||||||
source venv/bin/activate
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec python picodulce.py
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x "$PICODULCE_DIR/run.sh"
|
|
||||||
|
|
||||||
# --- Create .desktop entry ---
|
|
||||||
msg "Creating .desktop entry..."
|
|
||||||
mkdir -p "$(dirname "$DESKTOP_FILE")"
|
|
||||||
|
|
||||||
cat > "$DESKTOP_FILE" <<EOF
|
|
||||||
[Desktop Entry]
|
|
||||||
Name=Picodulce
|
|
||||||
Exec=$PICODULCE_DIR/run.sh
|
|
||||||
Icon=$PICODULCE_DIR/launcher_icon.ico
|
|
||||||
Terminal=true
|
|
||||||
Type=Application
|
|
||||||
Comment=Picodulce Launcher
|
|
||||||
Categories=Game;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# --- Ask if install in /usr/bin ---
|
|
||||||
echo
|
|
||||||
read -rp "Do you want to install the "picodulce" command? it requires sudo. (y/n) " choice
|
|
||||||
if [[ "$choice" =~ ^[Yy]$ ]]; then
|
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
|
||||||
echo "Root permissions required to install into /usr/bin"
|
|
||||||
sudo bash -c "cat > $BIN_FILE" <<EOF
|
|
||||||
#!/bin/bash
|
|
||||||
cd $PICODULCE_DIR
|
|
||||||
exec ./run.sh
|
|
||||||
EOF
|
|
||||||
sudo chmod +x "$BIN_FILE"
|
|
||||||
else
|
|
||||||
cat > "$BIN_FILE" <<EOF
|
|
||||||
#!/bin/bash
|
|
||||||
cd $PICODULCE_DIR
|
|
||||||
exec ./run.sh
|
|
||||||
EOF
|
|
||||||
chmod +x "$BIN_FILE"
|
|
||||||
fi
|
|
||||||
msg "Installed 'picodulce' command in /usr/bin"
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg "Installation complete!"
|
|
||||||
echo "You can run Picodulce with:"
|
|
||||||
echo " $PICODULCE_DIR/run.sh"
|
|
||||||
echo "Or from your applications menu."
|
|
||||||
367
loaddaemon.py
367
loaddaemon.py
@ -1,367 +0,0 @@
|
|||||||
import sys
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import shlex
|
|
||||||
import gc
|
|
||||||
import re
|
|
||||||
from io import StringIO
|
|
||||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QProgressBar, QPushButton, QHBoxLayout
|
|
||||||
from PyQt5.QtCore import Qt, pyqtSignal, QObject, QTimer, QEvent
|
|
||||||
from PyQt5.QtGui import QFont
|
|
||||||
|
|
||||||
|
|
||||||
class LaunchSignals(QObject):
|
|
||||||
log_update = pyqtSignal(str)
|
|
||||||
launch_complete = pyqtSignal()
|
|
||||||
launch_aborted = pyqtSignal()
|
|
||||||
cleanup_done = pyqtSignal()
|
|
||||||
|
|
||||||
|
|
||||||
class AbortException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class StreamingCapture(StringIO):
|
|
||||||
def __init__(self, log_signal, launch_signal):
|
|
||||||
super().__init__()
|
|
||||||
self.log_signal = log_signal
|
|
||||||
self.launch_signal = launch_signal
|
|
||||||
self.launch_detected = False
|
|
||||||
self.abort_requested = False
|
|
||||||
|
|
||||||
def write(self, text):
|
|
||||||
if self.abort_requested:
|
|
||||||
raise AbortException("Launch aborted by user")
|
|
||||||
|
|
||||||
if text and text.strip():
|
|
||||||
# Check if signal is still valid before emitting
|
|
||||||
try:
|
|
||||||
self.log_signal.emit(text.strip())
|
|
||||||
except RuntimeError:
|
|
||||||
# Signal/Object might be deleted
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not self.launch_detected and self.launch_signal:
|
|
||||||
lower_text = text.lower()
|
|
||||||
if "launching" in lower_text and ("game" in lower_text or "version" in lower_text or "minecraft" in lower_text):
|
|
||||||
self.launch_detected = True
|
|
||||||
try:
|
|
||||||
self.launch_signal.emit()
|
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return super().write(text)
|
|
||||||
|
|
||||||
|
|
||||||
class LaunchWindow(QDialog):
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setWindowTitle("Launching Minecraft")
|
|
||||||
self.setModal(True)
|
|
||||||
self.resize(400, 150)
|
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
|
|
||||||
self.status_label = QLabel("Initializing...")
|
|
||||||
self.status_label.setWordWrap(True)
|
|
||||||
self.status_label.setAlignment(Qt.AlignCenter)
|
|
||||||
layout.addWidget(self.status_label)
|
|
||||||
|
|
||||||
self.progress_bar = QProgressBar()
|
|
||||||
self.progress_bar.setRange(0, 0) # Indeterminate progress
|
|
||||||
self.progress_bar.setTextVisible(False)
|
|
||||||
layout.addWidget(self.progress_bar)
|
|
||||||
|
|
||||||
# Add a manual close/cancel button
|
|
||||||
button_layout = QHBoxLayout()
|
|
||||||
button_layout.addStretch()
|
|
||||||
self.cancel_button = QPushButton("Cancel")
|
|
||||||
self.cancel_button.clicked.connect(self.request_abort)
|
|
||||||
button_layout.addWidget(self.cancel_button)
|
|
||||||
layout.addLayout(button_layout)
|
|
||||||
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
self.signals = LaunchSignals()
|
|
||||||
self.signals.log_update.connect(self.update_status)
|
|
||||||
self.signals.launch_complete.connect(self.on_launch_complete)
|
|
||||||
self.signals.launch_aborted.connect(self.on_launch_aborted)
|
|
||||||
self.signals.cleanup_done.connect(self.on_cleanup_done)
|
|
||||||
|
|
||||||
self.launch_detected = False
|
|
||||||
self.closing_scheduled = False
|
|
||||||
self.aborting = False
|
|
||||||
self.capture_streams = []
|
|
||||||
self.thread_running = False
|
|
||||||
|
|
||||||
def update_status(self, text):
|
|
||||||
if len(text) > 100:
|
|
||||||
text = text[:97] + "..."
|
|
||||||
self.status_label.setText(text)
|
|
||||||
|
|
||||||
def on_launch_complete(self):
|
|
||||||
if not self.closing_scheduled and not self.aborting:
|
|
||||||
self.closing_scheduled = True
|
|
||||||
self.status_label.setText("Game Launched! Closing window...")
|
|
||||||
self.progress_bar.setRange(0, 100)
|
|
||||||
self.progress_bar.setValue(100)
|
|
||||||
self.cancel_button.setEnabled(False)
|
|
||||||
QTimer.singleShot(3000, self.accept)
|
|
||||||
|
|
||||||
def on_launch_aborted(self):
|
|
||||||
self.status_label.setText("Launch Aborted.")
|
|
||||||
self.progress_bar.setRange(0, 100)
|
|
||||||
self.progress_bar.setValue(0)
|
|
||||||
|
|
||||||
def on_cleanup_done(self):
|
|
||||||
self.thread_running = False
|
|
||||||
# Now it is safe to close the window
|
|
||||||
super().reject()
|
|
||||||
|
|
||||||
def request_abort(self):
|
|
||||||
if self.thread_running and not self.aborting:
|
|
||||||
self.aborting = True
|
|
||||||
self.status_label.setText("Aborting...")
|
|
||||||
self.cancel_button.setEnabled(False)
|
|
||||||
# Signal streams to stop
|
|
||||||
for stream in self.capture_streams:
|
|
||||||
stream.abort_requested = True
|
|
||||||
elif not self.thread_running:
|
|
||||||
super().reject()
|
|
||||||
|
|
||||||
def reject(self):
|
|
||||||
# Handle Esc key or X button
|
|
||||||
self.request_abort()
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
|
||||||
if self.thread_running:
|
|
||||||
event.ignore()
|
|
||||||
self.request_abort()
|
|
||||||
else:
|
|
||||||
event.accept()
|
|
||||||
|
|
||||||
def launch_game(self, command):
|
|
||||||
self.thread_running = True
|
|
||||||
thread = threading.Thread(target=self._run_launch, args=(command,), daemon=True)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
def _run_launch(self, command):
|
|
||||||
try:
|
|
||||||
modules_to_remove = [mod for mod in sys.modules if mod.startswith('zucaro')]
|
|
||||||
for mod in modules_to_remove:
|
|
||||||
del sys.modules[mod]
|
|
||||||
gc.collect()
|
|
||||||
|
|
||||||
from zucaro.cli.main import zucaro_cli
|
|
||||||
|
|
||||||
old_stdout, old_stderr = sys.stdout, sys.stderr
|
|
||||||
|
|
||||||
stdout_capture = StreamingCapture(self.signals.log_update, self.signals.launch_complete)
|
|
||||||
stderr_capture = StreamingCapture(self.signals.log_update, self.signals.launch_complete)
|
|
||||||
|
|
||||||
self.capture_streams = [stdout_capture, stderr_capture]
|
|
||||||
|
|
||||||
sys.stdout = stdout_capture
|
|
||||||
sys.stderr = stderr_capture
|
|
||||||
|
|
||||||
try:
|
|
||||||
zucaro_cli.main(args=shlex.split(command), standalone_mode=False)
|
|
||||||
except AbortException:
|
|
||||||
self.signals.launch_aborted.emit()
|
|
||||||
except SystemExit:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
self.signals.log_update.emit(f"Error: {str(e)}")
|
|
||||||
finally:
|
|
||||||
sys.stdout = old_stdout
|
|
||||||
sys.stderr = old_stderr
|
|
||||||
|
|
||||||
if not stdout_capture.launch_detected and not stderr_capture.launch_detected and not stdout_capture.abort_requested:
|
|
||||||
self.signals.launch_complete.emit()
|
|
||||||
|
|
||||||
modules_to_remove = [mod for mod in sys.modules if mod.startswith('zucaro')]
|
|
||||||
for mod in modules_to_remove:
|
|
||||||
del sys.modules[mod]
|
|
||||||
gc.collect()
|
|
||||||
|
|
||||||
self.signals.cleanup_done.emit()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
try:
|
|
||||||
self.signals.log_update.emit(f"Error launching game: {str(e)}")
|
|
||||||
self.signals.cleanup_done.emit()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PrepareWindow(QDialog):
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setWindowTitle("Preparing Version")
|
|
||||||
self.setModal(True)
|
|
||||||
self.resize(400, 150)
|
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
|
|
||||||
self.status_label = QLabel("Initializing...")
|
|
||||||
self.status_label.setWordWrap(True)
|
|
||||||
self.status_label.setAlignment(Qt.AlignCenter)
|
|
||||||
layout.addWidget(self.status_label)
|
|
||||||
|
|
||||||
self.progress_bar = QProgressBar()
|
|
||||||
self.progress_bar.setRange(0, 0) # Indeterminate progress
|
|
||||||
self.progress_bar.setTextVisible(False)
|
|
||||||
layout.addWidget(self.progress_bar)
|
|
||||||
|
|
||||||
# Add a manual close/cancel button
|
|
||||||
button_layout = QHBoxLayout()
|
|
||||||
button_layout.addStretch()
|
|
||||||
self.cancel_button = QPushButton("Cancel")
|
|
||||||
self.cancel_button.clicked.connect(self.request_abort)
|
|
||||||
button_layout.addWidget(self.cancel_button)
|
|
||||||
layout.addLayout(button_layout)
|
|
||||||
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
self.signals = LaunchSignals()
|
|
||||||
self.signals.log_update.connect(self.update_status)
|
|
||||||
self.signals.launch_complete.connect(self.on_prepare_complete)
|
|
||||||
self.signals.launch_aborted.connect(self.on_prepare_aborted)
|
|
||||||
self.signals.cleanup_done.connect(self.on_cleanup_done)
|
|
||||||
|
|
||||||
self.aborting = False
|
|
||||||
self.capture_streams = []
|
|
||||||
self.thread_running = False
|
|
||||||
self.success = False
|
|
||||||
|
|
||||||
def update_status(self, text):
|
|
||||||
# Parse output for progress updates
|
|
||||||
|
|
||||||
if "Downloading" in text and "libraries" in text:
|
|
||||||
try:
|
|
||||||
count = int(re.search(r'\d+', text).group())
|
|
||||||
self.status_label.setText(f"Downloading {count} libraries...")
|
|
||||||
except:
|
|
||||||
self.status_label.setText(text)
|
|
||||||
elif "Checking" in text and "assets" in text:
|
|
||||||
try:
|
|
||||||
count = int(re.search(r'\d+', text).group())
|
|
||||||
self.status_label.setText(f"Checking {count} assets...")
|
|
||||||
except:
|
|
||||||
self.status_label.setText(text)
|
|
||||||
elif "Jar file" in text and "downloaded" in text:
|
|
||||||
self.status_label.setText("Downloading game jar...")
|
|
||||||
elif "Checking libraries" in text:
|
|
||||||
self.status_label.setText("Checking libraries...")
|
|
||||||
else:
|
|
||||||
if len(text) > 100:
|
|
||||||
text = text[:97] + "..."
|
|
||||||
self.status_label.setText(text)
|
|
||||||
|
|
||||||
def on_prepare_complete(self):
|
|
||||||
if not self.aborting:
|
|
||||||
self.success = True
|
|
||||||
self.status_label.setText("Version prepared successfully!")
|
|
||||||
self.progress_bar.setRange(0, 100)
|
|
||||||
self.progress_bar.setValue(100)
|
|
||||||
self.cancel_button.setEnabled(False)
|
|
||||||
QTimer.singleShot(1500, self.accept)
|
|
||||||
|
|
||||||
def on_prepare_aborted(self):
|
|
||||||
self.status_label.setText("Preparation Aborted.")
|
|
||||||
self.progress_bar.setRange(0, 100)
|
|
||||||
self.progress_bar.setValue(0)
|
|
||||||
self.success = False
|
|
||||||
|
|
||||||
def on_cleanup_done(self):
|
|
||||||
self.thread_running = False
|
|
||||||
if not self.success:
|
|
||||||
super().reject()
|
|
||||||
|
|
||||||
def request_abort(self):
|
|
||||||
if self.thread_running and not self.aborting:
|
|
||||||
self.aborting = True
|
|
||||||
self.status_label.setText("Aborting...")
|
|
||||||
self.cancel_button.setEnabled(False)
|
|
||||||
# Signal streams to stop
|
|
||||||
for stream in self.capture_streams:
|
|
||||||
stream.abort_requested = True
|
|
||||||
elif not self.thread_running:
|
|
||||||
super().reject()
|
|
||||||
|
|
||||||
def reject(self):
|
|
||||||
self.request_abort()
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
|
||||||
if self.thread_running:
|
|
||||||
event.ignore()
|
|
||||||
self.request_abort()
|
|
||||||
else:
|
|
||||||
event.accept()
|
|
||||||
|
|
||||||
def prepare_version(self, version):
|
|
||||||
command = f"version prepare {version}"
|
|
||||||
self.thread_running = True
|
|
||||||
thread = threading.Thread(target=self._run_prepare, args=(command,), daemon=True)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
def _run_prepare(self, command):
|
|
||||||
try:
|
|
||||||
modules_to_remove = [mod for mod in sys.modules if mod.startswith('zucaro')]
|
|
||||||
for mod in modules_to_remove:
|
|
||||||
del sys.modules[mod]
|
|
||||||
gc.collect()
|
|
||||||
|
|
||||||
from zucaro.cli.main import zucaro_cli
|
|
||||||
|
|
||||||
old_stdout, old_stderr = sys.stdout, sys.stderr
|
|
||||||
|
|
||||||
stdout_capture = StreamingCapture(self.signals.log_update, None)
|
|
||||||
stderr_capture = StreamingCapture(self.signals.log_update, None)
|
|
||||||
|
|
||||||
self.capture_streams = [stdout_capture, stderr_capture]
|
|
||||||
|
|
||||||
sys.stdout = stdout_capture
|
|
||||||
sys.stderr = stderr_capture
|
|
||||||
|
|
||||||
try:
|
|
||||||
zucaro_cli.main(args=shlex.split(command), standalone_mode=False)
|
|
||||||
except AbortException:
|
|
||||||
self.signals.launch_aborted.emit()
|
|
||||||
except SystemExit:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
self.signals.log_update.emit(f"Error: {str(e)}")
|
|
||||||
finally:
|
|
||||||
sys.stdout = old_stdout
|
|
||||||
sys.stderr = old_stderr
|
|
||||||
|
|
||||||
if not stdout_capture.abort_requested:
|
|
||||||
self.signals.launch_complete.emit()
|
|
||||||
|
|
||||||
modules_to_remove = [mod for mod in sys.modules if mod.startswith('zucaro')]
|
|
||||||
for mod in modules_to_remove:
|
|
||||||
del sys.modules[mod]
|
|
||||||
gc.collect()
|
|
||||||
|
|
||||||
self.signals.cleanup_done.emit()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
try:
|
|
||||||
self.signals.log_update.emit(f"Error preparing version: {str(e)}")
|
|
||||||
self.signals.cleanup_done.emit()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def launch_instance_with_window(command, parent=None):
|
|
||||||
window = LaunchWindow(parent)
|
|
||||||
window.launch_game(command)
|
|
||||||
window.exec_()
|
|
||||||
return window
|
|
||||||
|
|
||||||
def prepare_version_with_window(version, parent=None):
|
|
||||||
window = PrepareWindow(parent)
|
|
||||||
window.prepare_version(version)
|
|
||||||
result = window.exec_()
|
|
||||||
return window.success
|
|
||||||
46
modulecli.py
46
modulecli.py
@ -1,46 +0,0 @@
|
|||||||
from io import StringIO
|
|
||||||
import sys
|
|
||||||
import shlex
|
|
||||||
import gc
|
|
||||||
|
|
||||||
def run_command(command="zucaro"):
|
|
||||||
# Remove all zucaro-related modules from sys.modules BEFORE import
|
|
||||||
modules_to_remove = [mod for mod in sys.modules if mod.startswith('zucaro')]
|
|
||||||
for mod in modules_to_remove:
|
|
||||||
del sys.modules[mod]
|
|
||||||
gc.collect()
|
|
||||||
|
|
||||||
# Import zucaro_cli dynamically
|
|
||||||
from zucaro.cli.main import zucaro_cli
|
|
||||||
|
|
||||||
# Redirect stdout and stderr to capture the command output
|
|
||||||
old_stdout, old_stderr = sys.stdout, sys.stderr
|
|
||||||
sys.stdout = mystdout = StringIO()
|
|
||||||
sys.stderr = mystderr = StringIO()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Use shlex.split to properly parse the command string
|
|
||||||
# This will call Click's CLI as if from command line, using args
|
|
||||||
zucaro_cli.main(args=shlex.split(command))
|
|
||||||
except SystemExit as e:
|
|
||||||
if e.code != 0:
|
|
||||||
print(f"Command exited with code {e.code}", file=sys.stderr)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Unexpected error: {e}", file=sys.stderr)
|
|
||||||
finally:
|
|
||||||
# Restore stdout and stderr
|
|
||||||
sys.stdout = old_stdout
|
|
||||||
sys.stderr = old_stderr
|
|
||||||
|
|
||||||
output = mystdout.getvalue().strip()
|
|
||||||
error = mystderr.getvalue().strip()
|
|
||||||
|
|
||||||
# Cleanup: remove zucaro-related modules from sys.modules and force garbage collection
|
|
||||||
modules_to_remove = [mod for mod in sys.modules if mod.startswith('zucaro')]
|
|
||||||
for mod in modules_to_remove:
|
|
||||||
del sys.modules[mod]
|
|
||||||
gc.collect()
|
|
||||||
|
|
||||||
if not output:
|
|
||||||
return None
|
|
||||||
return output
|
|
||||||
1278
picodulce.py
1278
picodulce.py
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,4 @@
|
|||||||
zucaro
|
picomc
|
||||||
PyQt5
|
PyQt5
|
||||||
requests
|
requests
|
||||||
aiohttp
|
|
||||||
pypresence
|
pypresence
|
||||||
tqdm
|
|
||||||
|
|||||||
21
version.json
21
version.json
@ -1,15 +1,12 @@
|
|||||||
{
|
{
|
||||||
"version": "0.13.10",
|
"version": "0.11.4",
|
||||||
|
"name": "Lucky Bastard",
|
||||||
"links": [
|
"links": [
|
||||||
"https://raw.githubusercontent.com/nixietab/picodulce/main/version.json",
|
"https://raw.githubusercontent.com/nixietab/picodulce/canary/version.json",
|
||||||
"https://raw.githubusercontent.com/nixietab/picodulce/main/picodulce.py",
|
"https://raw.githubusercontent.com/nixietab/picodulce/canary/picodulce.py",
|
||||||
"https://raw.githubusercontent.com/nixietab/picodulce/main/requirements.txt",
|
"https://raw.githubusercontent.com/nixietab/picodulce/canary/requirements.txt",
|
||||||
"https://raw.githubusercontent.com/nixietab/picodulce/main/marroc.py",
|
"https://raw.githubusercontent.com/nixietab/picodulce/canary/drums.gif",
|
||||||
"https://raw.githubusercontent.com/nixietab/picodulce/main/holiday.ico",
|
"https://raw.githubusercontent.com/nixietab/picodulce/canary/marroc.py",
|
||||||
"https://raw.githubusercontent.com/nixietab/picodulce/main/authser.py",
|
"https://raw.githubusercontent.com/nixietab/picodulce/canary/holiday.ico"
|
||||||
"https://raw.githubusercontent.com/nixietab/picodulce/main/healthcheck.py",
|
]
|
||||||
"https://raw.githubusercontent.com/nixietab/picodulce/main/modulecli.py",
|
|
||||||
"https://raw.githubusercontent.com/nixietab/picodulce/main/loaddaemon.py"
|
|
||||||
],
|
|
||||||
"versionBleeding": "0.13.3-212"
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user