Merge diamond into main (#175)

Co-authored-by: str1k3r <115313679+S1l3ntStr1ke87@users.noreply.github.com>
This commit is contained in:
/home/neo 2026-08-17 21:42:38 +03:00 committed by GitHub
parent 816997803a
commit d6f622c913
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
128 changed files with 5747 additions and 1513 deletions

View file

@ -167,3 +167,80 @@ jobs:
with:
name: appimage
path: pkg/AppImage/dist/*.AppImage
build-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: "pnpm"
- name: setup Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: setup Android SDK
uses: android-actions/setup-android@v4
- name: install Android SDK components
run: |
sdkmanager --install "platforms;android-36" "build-tools;36.0.0" "ndk;28.2.13676358"
yes | sdkmanager --licenses
sudo apt-get install -y glslang-tools
- name: install Rust stable with android target
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "./src-tauri -> target"
- name: install frontend dependencies
run: pnpm install
- name: download android bundled assets
run: pnpm android:assets
- name: restore gradle wrapper jar
run: |
VER=$(sed -n 's/.*gradle-\([0-9][0-9.]*\)-bin\.zip.*/\1/p' src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties)
curl -fL -o src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar \
"https://raw.githubusercontent.com/gradle/gradle/v${VER}/gradle/wrapper/gradle-wrapper.jar"
- name: set up android signing keystore
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
run: |
printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 -d > src-tauri/gen/android/app/release.keystore
cat > src-tauri/gen/android/app/key.properties <<EOF
keyAlias=$ANDROID_KEY_ALIAS
keyPassword=$ANDROID_KEYSTORE_PASSWORD
storeFile=release.keystore
storePassword=$ANDROID_KEYSTORE_PASSWORD
EOF
- name: build Android APK
env:
ANDROID_NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/28.2.13676358
NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/28.2.13676358
run: npx tauri android build --target aarch64
- name: upload APK
uses: actions/upload-artifact@v4
with:
name: android-apk
path: src-tauri/gen/android/app/build/outputs/apk/universal/release/*.apk

View file

@ -139,8 +139,87 @@ jobs:
name: appimage
path: pkg/AppImage/dist/*.AppImage* #neo: *.AppImage* because .zsync as well
build-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: "pnpm"
- name: setup Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: setup Android SDK
uses: android-actions/setup-android@v4
- name: install Android SDK components
run: |
sdkmanager --install "platforms;android-36" "build-tools;36.0.0" "ndk;28.2.13676358"
yes | sdkmanager --licenses
sudo apt-get install -y glslang-tools
- name: install Rust stable with android target
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "./src-tauri -> target"
- name: install frontend dependencies
run: pnpm install
- name: download android bundled assets
run: pnpm android:assets
- name: restore gradle wrapper jar
run: |
VER=$(sed -n 's/.*gradle-\([0-9][0-9.]*\)-bin\.zip.*/\1/p' src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties)
curl -fL -o src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar \
"https://raw.githubusercontent.com/gradle/gradle/v${VER}/gradle/wrapper/gradle-wrapper.jar"
- name: set up android signing keystore
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
run: |
printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 -d > src-tauri/gen/android/app/release.keystore
cat > src-tauri/gen/android/app/key.properties <<EOF
keyAlias=$ANDROID_KEY_ALIAS
keyPassword=$ANDROID_KEYSTORE_PASSWORD
storeFile=release.keystore
storePassword=$ANDROID_KEYSTORE_PASSWORD
EOF
- name: build Android APK
env:
ANDROID_NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/28.2.13676358
NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/28.2.13676358
run: npx tauri android build --target aarch64
- name: upload APK
uses: actions/upload-artifact@v4
with:
name: android-apk
path: src-tauri/gen/android/app/build/outputs/apk/universal/release/*.apk
publish-flatpak:
needs: [get-version, build-appimage]
needs: [get-version, build-appimage, build-android]
runs-on: ubuntu-latest
permissions:
contents: write
@ -162,6 +241,10 @@ jobs:
with:
name: appimage
path: appimage
- uses: actions/download-artifact@v4
with:
name: android-apk
path: android
- uses: softprops/action-gh-release@v2
with:
body: ${{ needs.get-version.outputs.body }}
@ -169,6 +252,7 @@ jobs:
files: |
emerald.flatpak
appimage/*.AppImage*
android/*.apk
draft: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

5
.gitignore vendored
View file

@ -36,3 +36,8 @@ Thumbs.db
/*.flatpak
/emerald-repo
/ignore/*
# Android bundled runtime assets (too large for GitHub; must be present in the working tree)
/src-tauri/gen/android/app/src/main/assets/imagefs.tar.zst
/src-tauri/gen/android/app/src/main/assets/proton-*.tar.zst

3
.gitmodules vendored
View file

@ -1,3 +1,6 @@
[submodule "flatpak/shared-modules"]
path = flatpak/shared-modules
url = https://github.com/flathub/shared-modules.git
[submodule "DiamondRuntime"]
path = DiamondRuntime
url = https://github.com/LCE-Hub/DiamondRuntime.git

View file

@ -37,3 +37,12 @@ pnpm flatpak # or npm
## Gentoo
A Portage overlay lives in [`gentoo/`](gentoo/). See the GNU/Linux installation section in [README.md](README.md).
## Android
Building Emerald on Android is pretty easy,
```sh
pnpm android:assets #neo: fetches the rootfs and proton11
pnpm tauri android build -t aarch64
```

1
DiamondRuntime Submodule

@ -0,0 +1 @@
Subproject commit c3f56e880c4c59fe591fa2bf863ea6f247886749

View file

@ -3,7 +3,7 @@
<h1>LCE Emerald Launcher</h1>
<p><strong>FOSS cross-platform launcher for Minecraft Legacy Console Edition</strong></p>
<p>
<img src="https://img.shields.io/badge/version-1.5.1-blue?style=flat-square" alt="Version">
<img src="https://img.shields.io/badge/version-1.6.0-blue?style=flat-square" alt="Version">
<img src="https://img.shields.io/badge/license-GPL--3.0-green?style=flat-square" alt="License">
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20GNU/Linux-lightgrey?style=flat-square" alt="Platforms">
</p>
@ -163,7 +163,7 @@ This repository includes a local Portage overlay under [`gentoo/`](gentoo/). Poi
location = /path/to/LCE-Emerald-Launcher/gentoo
auto-sync = no
sudo emerge games-util/emerald-legacy-launcher # 1.5.1
sudo emerge games-util/emerald-legacy-launcher # 1.6.0
# sudo emerge =games-util/emerald-legacy-launcher-9999 # live git
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

View file

@ -1,11 +1,25 @@
![Release Artwork](https://raw.githubusercontent.com/LCE-Hub/LCE-Emerald-Launcher/refs/heads/main/RELEASE_ARTWORK.png)
- Fix custom path downloading overwriting non-empty directories ([#127](https://github.com/LCE-Hub/LCE-Emerald-Launcher/issues/127))
- Add support for version channels in Workshop
- Fix LCEOnline Auth on Linux and macOS
- Add `unzip` fallback for Workshop items on Linux
- Fix critical bug in Workshop that deleted worlds and DLCs (thanks LeGamer!)
- Fix random disconnects on Windows
- Show multiplayer status on Discord RPC
- Fix skipping intro, fullscreen and legacy mode not importing in Settings (thanks Ryuzaki!)
- Remove controls menu from Settings
- Fix title bar and panorama not showing on setup
- **Partnership with LapboardMC!**
- **Removed LCEOnline Client, LCEOnline functionality is built into Emerald**
- Fix audio skipping bug on Windows
- Gentoo, Nix flakes and Homebrew support
- **Xbox 360, PS3 and Java -> LCE world importing support (beta!)**
- Proper Discord RPC shutdown
- Remove click effects when disabling animations
- **FULL Android Port powered by our own DiamondRuntime for running the game!**
- Fix neoLegacy downloading issue
- **A lot of UI polish (thanks piebot!)**
- Add 2 default capes
- Add more default skins
- **Emerald Arguments.Schema support!**
- **Partnership with Amythest LCE**
- Fix Slim skins rendering in-launcher
- Randomize splash texts
- **Partnership with Project Lost Legacy (soon!)**
- Add Player Head to Discord RPC
- Make skin face inwards instead of outwards
- Game crash log viewer support!
- **Intel macOS support**
- And more that I forgot!

View file

@ -4,6 +4,14 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script>
if (/Android/i.test(navigator.userAgent)) {
document.documentElement.classList.add("is-android");
document
.querySelector('meta[name="viewport"]')
.setAttribute("content", "width=1280");
}
</script>
<link rel="preload" href="/fonts/Mojangles.ttf" as="font" type="font/truetype" crossorigin>
<link rel="preload" href="/images/MenuTitle.png" as="image">
<title>Emerald Legacy Launcher</title>

View file

@ -10,6 +10,7 @@
"tauri": "tauri",
"tauri:build": "tauri build && pnpm run post-build:macos",
"post-build:macos": "bash src-tauri/scripts/post-build-macos.sh",
"android:assets": "node scripts/download-android-assets.mjs",
"flatpak": "flatpak-builder --user --install-deps-from=flathub --repo=emerald-repo --force-clean build-flatpak flatpak/io.github.Emerald_Legacy_Launcher.Emerald_Legacy_Launcher.yml && flatpak build-bundle emerald-repo emerald.flatpak io.github.Emerald_Legacy_Launcher.Emerald_Legacy_Launcher"
},
"dependencies": {

BIN
public/Capes/4J.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

BIN
public/Skins/ava.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/Skins/byte.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/Skins/counterr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/Skins/tranq.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

BIN
public/images/amythest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

260
schemas/args.json Normal file
View file

@ -0,0 +1,260 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://emerald-legacy-launcher/schemas/args.json",
"title": "ArgsSchema",
"description": "Schema describing launcher command-line argument options, groups, and conditional dependencies.",
"type": "object",
"additionalProperties": false,
"required": ["$schema", "groups", "options", "dependencies"],
"properties": {
"$schema": {
"type": "string",
"description": "URI of the JSON schema this file conforms to."
},
"schemaVersion": {
"type": "integer",
"const": 1,
"description": "Version of the schema format. Currently always 1."
},
"meta": {
"type": "object",
"additionalProperties": true,
"description": "Arbitrary metadata about this schema."
},
"groups": {
"type": "array",
"description": "Groups used to organize options.",
"items": {
"$ref": "#/$defs/schemaGroup"
}
},
"options": {
"type": "array",
"description": "The command-line options.",
"items": {
"$ref": "#/$defs/schemaOption"
}
},
"dependencies": {
"type": "array",
"description": "Conditional dependencies that disable or hide options.",
"items": {
"$ref": "#/$defs/schemaDependency"
}
}
},
"$defs": {
"schemaGroup": {
"type": "object",
"additionalProperties": false,
"required": ["id", "title"],
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the group."
},
"title": {
"type": "string",
"description": "Display title of the group."
},
"description": {
"type": "string",
"description": "Optional description of the group."
}
}
},
"schemaOption": {
"type": "object",
"additionalProperties": false,
"required": ["id", "title", "type", "arg"],
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the option."
},
"title": {
"type": "string",
"description": "Display title of the option."
},
"type": {
"enum": ["boolean", "int", "number", "string", "choice"],
"description": "The kind of value the option accepts."
},
"arg": {
"type": "string",
"description": "The command-line argument string emitted for this option."
},
"description": {
"type": "string",
"description": "Optional human-readable description."
},
"group": {
"type": "string",
"description": "Identifier of the group this option belongs to."
},
"default": {
"description": "Default value for the option. Must match the option's type."
},
"min": {
"type": "number",
"description": "Minimum value for int/number options."
},
"max": {
"type": "number",
"description": "Maximum value for int/number options."
},
"step": {
"type": "number",
"description": "Stepping increment for int/number options."
},
"placeholder": {
"type": "string",
"description": "Placeholder text for string options."
},
"choices": {
"type": "array",
"description": "Allowed choices. Required and must be non-empty when type is 'choice'.",
"minItems": 1,
"items": {
"$ref": "#/$defs/schemaChoice"
}
}
},
"allOf": [
{
"if": {
"properties": {
"type": { "const": "choice" }
},
"required": ["type"]
},
"then": {
"required": ["choices"]
}
},
{
"if": {
"properties": {
"type": { "const": "boolean" }
},
"required": ["type"]
},
"then": {
"properties": {
"default": { "type": "boolean" }
}
}
},
{
"if": {
"properties": {
"type": { "const": "string" }
},
"required": ["type"]
},
"then": {
"properties": {
"default": { "type": "string" }
}
}
}
]
},
"schemaChoice": {
"type": "object",
"additionalProperties": false,
"required": ["value"],
"properties": {
"value": {
"type": "string",
"description": "The value stored/emitted for this choice."
},
"label": {
"type": "string",
"description": "Optional display label. Defaults to the value."
}
}
},
"schemaDependency": {
"type": "object",
"additionalProperties": false,
"required": ["target", "when"],
"properties": {
"target": {
"type": "string",
"description": "Identifier of the option affected by this dependency."
},
"when": {
"$ref": "#/$defs/condition",
"description": "Condition that, when true, applies the effect."
},
"effect": {
"enum": ["disable", "hide"],
"description": "Whether the target option is disabled or hidden. Defaults to 'disable'."
}
}
},
"condition": {
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"required": ["option"],
"properties": {
"option": {
"type": "string",
"description": "Identifier of the option to inspect."
},
"equals": {
"description": "True when the option value strictly equals this."
},
"in": {
"type": "array",
"description": "True when the option value is one of these.",
"items": true
},
"not": {
"description": "True when the option value is not strictly equal to this."
},
"exists": {
"type": "boolean",
"description": "True when the option value is present (or absent when false)."
}
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["all"],
"properties": {
"all": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/condition" }
}
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["any"],
"properties": {
"any": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/condition" }
}
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["not"],
"properties": {
"not": { "$ref": "#/$defs/condition" }
}
}
]
}
}
}

View file

@ -0,0 +1,109 @@
#!/usr/bin/env node
import {
createWriteStream,
createReadStream,
existsSync,
unlinkSync,
} from "node:fs";
import { mkdir, rename } from "node:fs/promises";
import { createHash } from "node:crypto";
import { Readable } from "node:stream";
import { fileURLToPath } from "node:url";
import path from "node:path";
const RELEASE_TAG = "android-assets-v1";
const REPO = "LCE-Hub/LCE-Emerald-Launcher";
const ASSET_DIR = fileURLToPath(
new URL("../src-tauri/gen/android/app/src/main/assets/", import.meta.url),
);
const ASSETS = [
{
file: "imagefs.tar.zst",
sha256: "7838756e6a05c91afff68f4bf12aa2780f815877753f8dd354e203e99b9caf8a",
},
{
file: "proton-11.0-arm64ec.tar.zst",
sha256: "ceb768396c2d44256518f94ae236f0c1f40738cf050a52e990e2fef67ff161df",
},
];
function sha256File(file) {
return new Promise((resolve, reject) => {
const hash = createHash("sha256");
const stream = createReadStream(file);
stream.on("data", (chunk) => hash.update(chunk));
stream.on("end", () => resolve(hash.digest("hex")));
stream.on("error", reject);
});
}
async function download(url, dest) {
const res = await fetch(url, { redirect: "follow" });
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText} for ${url}`);
}
if (!res.body) {
throw new Error(`no response body for ${url}`);
}
const fileStream = createWriteStream(dest);
await new Promise((resolve, reject) => {
const body = Readable.fromWeb(res.body);
body.pipe(fileStream);
body.on("error", reject);
fileStream.on("finish", resolve);
fileStream.on("error", reject);
});
}
let failed = false;
for (const asset of ASSETS) {
const target = path.join(ASSET_DIR, asset.file);
if (existsSync(target)) {
const actual = await sha256File(target);
if (actual === asset.sha256) {
console.log(`okay ${asset.file} (up to date)`);
continue;
}
console.error(
`error ${asset.file}: sha256 mismatch (expected ${asset.sha256}, got ${actual}); delete it and rerun`,
);
failed = true;
continue;
}
const url = `https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${asset.file}`;
console.log(`get ${asset.file} <- ${url}`);
await mkdir(path.dirname(target), { recursive: true });
const tmp = target + ".part";
try {
await download(url, tmp);
const actual = await sha256File(tmp);
if (actual !== asset.sha256) {
console.error(
`error ${asset.file}: downloaded sha256 mismatch (expected ${asset.sha256}, got ${actual})`,
);
failed = true;
if (existsSync(tmp)) {
unlinkSync(tmp);
}
continue;
}
await rename(tmp, target);
console.log(`okay ${asset.file} (downloaded)`);
} catch (error) {
console.error(`error ${asset.file}: ${error.message}`);
failed = true;
if (existsSync(tmp)) {
unlinkSync(tmp);
}
}
}
process.exit(failed ? 1 : 0);

252
src-tauri/Cargo.lock generated
View file

@ -635,7 +635,7 @@ dependencies = [
"bitflags 2.11.0",
"core-foundation 0.10.1",
"core-graphics-types",
"foreign-types 0.5.0",
"foreign-types",
"libc",
]
@ -1051,6 +1051,7 @@ dependencies = [
"futures-util",
"http 1.4.0",
"image 0.24.9",
"jni",
"libc",
"lzxd",
"once_cell",
@ -1074,6 +1075,7 @@ dependencies = [
"tokio-util",
"url",
"uuid 1.23.0",
"wry",
]
[[package]]
@ -1264,15 +1266,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared 0.1.1",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
@ -1280,7 +1273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared 0.3.1",
"foreign-types-shared",
]
[[package]]
@ -1294,12 +1287,6 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
@ -1926,6 +1913,12 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "http-range"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
[[package]]
name = "httparse"
version = "1.10.1"
@ -1982,6 +1975,20 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper 0.14.32",
"rustls 0.21.12",
"tokio",
"tokio-rustls 0.24.1",
]
[[package]]
name = "hyper-rustls"
version = "0.27.9"
@ -1991,25 +1998,12 @@ dependencies = [
"http 1.4.0",
"hyper 1.9.0",
"hyper-util",
"rustls",
"rustls 0.23.40",
"tokio",
"tokio-rustls",
"tokio-rustls 0.26.4",
"tower-service",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper 0.14.32",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
@ -2686,23 +2680,6 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "native-tls"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "ndk"
version = "0.9.0"
@ -2980,50 +2957,12 @@ dependencies = [
"pathdiff",
]
[[package]]
name = "openssl"
version = "0.10.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf"
dependencies = [
"bitflags 2.11.0",
"cfg-if",
"foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-sys"
version = "0.9.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@ -3781,15 +3720,15 @@ dependencies = [
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.32",
"hyper-tls",
"hyper-rustls 0.24.2",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls 0.21.12",
"rustls-pemfile",
"serde",
"serde_json",
@ -3797,7 +3736,7 @@ dependencies = [
"sync_wrapper 0.1.2",
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.24.1",
"tokio-util",
"tower-service",
"url",
@ -3805,6 +3744,7 @@ dependencies = [
"wasm-bindgen-futures",
"wasm-streams 0.4.2",
"web-sys",
"webpki-roots 0.25.4",
"winreg 0.50.0",
]
@ -3822,20 +3762,20 @@ dependencies = [
"http-body 1.0.1",
"http-body-util",
"hyper 1.9.0",
"hyper-rustls",
"hyper-rustls 0.27.9",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"rustls",
"rustls 0.23.40",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper 1.0.2",
"tokio",
"tokio-rustls",
"tokio-rustls 0.26.4",
"tokio-util",
"tower",
"tower-http",
@ -3936,6 +3876,32 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log",
"ring",
"rustls-webpki 0.101.7",
"sct",
]
[[package]]
name = "rustls"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
dependencies = [
"log",
"ring",
"rustls-pki-types",
"rustls-webpki 0.102.8",
"subtle",
"zeroize",
]
[[package]]
name = "rustls"
version = "0.23.40"
@ -3945,7 +3911,7 @@ dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"rustls-webpki 0.103.13",
"subtle",
"zeroize",
]
@ -3991,10 +3957,10 @@ dependencies = [
"jni",
"log",
"once_cell",
"rustls",
"rustls 0.23.40",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"rustls-webpki 0.103.13",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
@ -4007,6 +3973,27 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.103.13"
@ -4111,6 +4098,16 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sct"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "security-framework"
version = "3.7.0"
@ -4751,6 +4748,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http 1.4.0",
"http-range",
"image 0.25.10",
"jni",
"libc",
@ -4983,7 +4981,7 @@ dependencies = [
"osakit",
"percent-encoding",
"reqwest 0.13.2",
"rustls",
"rustls 0.23.40",
"semver",
"serde",
"serde_json",
@ -5263,12 +5261,23 @@ dependencies = [
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"native-tls",
"rustls 0.21.12",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
dependencies = [
"rustls 0.22.4",
"rustls-pki-types",
"tokio",
]
@ -5278,7 +5287,7 @@ version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"rustls 0.23.40",
"tokio",
]
@ -5290,10 +5299,12 @@ checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
dependencies = [
"futures-util",
"log",
"native-tls",
"rustls 0.22.4",
"rustls-pki-types",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.25.0",
"tungstenite",
"webpki-roots 0.26.11",
]
[[package]]
@ -5531,8 +5542,9 @@ dependencies = [
"http 1.4.0",
"httparse",
"log",
"native-tls",
"rand 0.8.5",
"rustls 0.22.4",
"rustls-pki-types",
"sha1",
"thiserror 1.0.69",
"url",
@ -5691,12 +5703,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vec_map"
version = "0.8.2"
@ -6034,6 +6040,30 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "webpki-roots"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
"webpki-roots 1.0.9",
]
[[package]]
name = "webpki-roots"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webview2-com"
version = "0.38.2"

View file

@ -13,22 +13,20 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["tray-icon", "image-png"] }
tauri = { version = "2", features = ["protocol-asset", "tray-icon", "image-png"] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-gamepad = "0.0.5"
tauri-plugin-drpc = "*"
reqwest = { version = "0.11", features = ["stream", "json"] }
reqwest = { version = "0.11", default-features = false, features = ["stream", "json", "rustls-tls-webpki-roots"] }
tokio = { version = "1", features = ["full"] }
futures-util = "0.3"
tokio-util = { version = "0.7.18", features = ["rt"] }
base64 = "0.21"
tokio-tungstenite = { version = "0.21", default-features = false, features = ["connect", "native-tls"] }
tokio-tungstenite = { version = "0.21", default-features = false, features = ["connect", "rustls-tls-webpki-roots"] }
url = "2"
rand = "0.8"
http = "1"
rfd = "0.15"
libc = "0.2"
image = "0.24"
steam_shortcuts_util = "1.1.8"
@ -39,10 +37,16 @@ once_cell = "1"
lzxd = "0.2.6"
uuid = { version = "1", features = ["v4"] }
tauri-plugin-deep-link = "2"
tauri-plugin-single-instance = "2"
[target.'cfg(target_os = "android")'.dependencies]
wry = { version = "0.54", default-features = false }
jni = "0.21"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-updater = "2"
tauri-plugin-drpc = "*"
tauri-plugin-single-instance = "2"
rfd = "0.15"
#neo: enable portable appimages. revert back to crates.io once the PR is merged
#[patch.crates-io]

View file

@ -1,6 +1,7 @@
{
"identifier": "default",
"description": "Default permissions",
"platforms": ["windows", "linux", "macOS"],
"windows": ["main"],
"permissions": [
"core:default",

View file

@ -0,0 +1,29 @@
{
"identifier": "mobile-capability",
"description": "Mobile permissions",
"platforms": ["android", "iOS"],
"windows": ["main"],
"permissions": [
"core:default",
"core:app:default",
"core:event:default",
"core:image:default",
"core:path:default",
"core:resources:default",
"core:webview:default",
"core:webview:allow-create-webview-window",
"core:window:default",
"opener:default",
{
"identifier": "opener:allow-open-path",
"allow": [
{
"path": "**"
}
]
},
"gamepad:default",
"process:default",
"deep-link:default"
]
}

View file

@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

20
src-tauri/gen/android/.gitignore vendored Normal file
View file

@ -0,0 +1,20 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
build
/captures
.externalNativeBuild
.cxx
local.properties
key.properties
release.keystore
/.tauri
/tauri.settings.gradle

6
src-tauri/gen/android/app/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
/src/main/**/generated
/src/main/jniLibs/**/*.so
/src/main/assets/tauri.conf.json
/tauri.build.gradle.kts
/proguard-tauri.pro
/tauri.properties

View file

@ -0,0 +1,93 @@
import java.util.Properties
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rust")
}
val tauriProperties = Properties().apply {
val propFile = file("tauri.properties")
if (propFile.exists()) {
propFile.inputStream().use { load(it) }
}
}
val keystoreProperties = Properties().apply {
val propFile = file("key.properties")
if (propFile.exists()) {
propFile.inputStream().use { load(it) }
}
}
android {
signingConfigs {
create("release") {
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
storeFile = file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
}
}
compileSdk = 36
namespace = "com.emerald.legacy"
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "com.emerald.legacy"
minSdk = 26
targetSdk = 28
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
proguardFiles(
*fileTree(".") { include("**/*.pro") }
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
.toList().toTypedArray()
)
}
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
buildConfig = true
}
lint {
disable += "ExpiredTargetSdkVersion"
}
packaging {
resources.excludes += "lib/*/jni/**"
}
}
rust {
rootDirRel = "../../../"
}
dependencies {
implementation(project(":diamondruntime"))
implementation("androidx.webkit:webkit:1.14.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("com.google.android.material:material:1.12.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}
apply(from = "tauri.build.gradle.kts")

View file

@ -0,0 +1,19 @@
-keep class dev.lcehub.emerald.** { *; }
-keepclassmembers class dev.lcehub.emerald.** { *; }
-dontwarn dev.lcehub.emerald.**
-keep class com.github.luben.zstd.** { *; }
-keepclassmembers class com.github.luben.zstd.** { *; }
-dontwarn com.github.luben.zstd.**
-keep class com.emerald.legacy.TauriActivity {
public <methods>;
public <fields>;
}
-keep class com.emerald.legacy.MainActivity {
public <methods>;
}
-dontwarn com.android.org.conscrypt.SSLParametersImpl
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl
-dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<!-- AndroidTV support -->
<uses-feature android:name="android.software.leanback" android:required="false" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.emerald_legacy_launcher"
android:extractNativeLibs="true"
android:usesCleartextTraffic="${usesCleartextTraffic}"
tools:replace="android:label,android:theme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:launchMode="singleTask"
android:screenOrientation="sensorLandscape"
android:label="@string/main_activity_title"
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- AndroidTV support -->
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
</activity>
<activity
android:name=".LceAuthActivity"
android:exported="false"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true"
tools:replace="android:authorities">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View file

@ -0,0 +1,124 @@
package com.emerald.legacy
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.os.Message
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
class LceAuthActivity : AppCompatActivity() {
private lateinit var webView: WebView
private lateinit var progressBar: ProgressBar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val authUrl = intent.getStringExtra("authUrl")
?: "https://mclegacyedition.xyz/internal/auth?appId=emerald_launcher"
progressBar = ProgressBar(this).apply {
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
webView = WebView(this).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.databaseEnabled = true
settings.loadWithOverviewMode = true
settings.useWideViewPort = true
settings.setSupportMultipleWindows(true)
settings.javaScriptCanOpenWindowsAutomatically = true
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean = handleAuthUrl(url)
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean = handleAuthUrl(request.url.toString())
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
handleAuthUrl(url)
}
override fun onPageFinished(view: WebView, url: String) {
progressBar.visibility = View.GONE
}
}
webChromeClient = object : android.webkit.WebChromeClient() {
override fun onCreateWindow(
view: WebView,
isDialog: Boolean,
isUserGesture: Boolean,
resultMsg: Message
): Boolean {
val transport = resultMsg.obj as WebView.WebViewTransport
transport.webView = webView
resultMsg.sendToTarget()
return true
}
}
}
val root = FrameLayout(this)
root.addView(
webView,
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
)
val progressFrame = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
progressFrame.gravity = Gravity.CENTER
root.addView(progressBar, progressFrame)
setContentView(root)
webView.loadUrl(authUrl)
}
private fun handleAuthUrl(url: String): Boolean {
val uri = try {
Uri.parse(url)
} catch (e: Exception) {
return false
}
if (uri.scheme == "emerald" && uri.host == "lceonline") {
val token = uri.getQueryParameter("token")
if (token != null) {
setResult(RESULT_OK, Intent().putExtra("lceAuthToken", token))
} else {
setResult(RESULT_CANCELED)
}
finish()
return true
}
return false
}
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
if (webView.canGoBack()) {
webView.goBack()
} else {
setResult(RESULT_CANCELED)
super.onBackPressed()
}
}
override fun onDestroy() {
(webView.parent as? ViewGroup)?.removeView(webView)
webView.destroy()
super.onDestroy()
}
}

View file

@ -0,0 +1,37 @@
package com.emerald.legacy
import android.app.Activity
import android.content.Intent
import androidx.activity.result.ActivityResult
import app.tauri.annotation.ActivityCallback
import app.tauri.annotation.Command
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Invoke
import app.tauri.plugin.Plugin
@TauriPlugin
class LceAuthPlugin(private val activity: Activity) : Plugin(activity) {
@Command
fun startAuth(invoke: Invoke) {
val intent = Intent(activity, LceAuthActivity::class.java)
intent.putExtra(
"authUrl",
"https://mclegacyedition.xyz/internal/auth?appId=emerald_launcher"
)
startActivityForResult(invoke, intent, "authResult")
}
@ActivityCallback
fun authResult(invoke: Invoke, result: ActivityResult) {
val token = if (result.resultCode == Activity.RESULT_OK) {
result.data?.getStringExtra("lceAuthToken")
} else {
null
}
if (!token.isNullOrEmpty()) {
invoke.resolveObject(token)
} else {
invoke.reject("Authentication cancelled or failed")
}
}
}

View file

@ -0,0 +1,28 @@
package com.emerald.legacy
import android.os.Bundle
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
WindowCompat.getInsetsController(window, window.decorView).apply {
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
}
}
override fun onWebViewCreate(webView: WebView) {
super.onWebViewCreate(webView)
webView.settings.useWideViewPort = true
webView.settings.loadWithOverviewMode = true
webView.settings.setSupportZoom(false)
webView.settings.builtInZoomControls = false
webView.settings.displayZoomControls = false
}
}

View file

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#00591C"
android:pathData="M0,0h108v108h-108z" />
</vector>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View file

@ -0,0 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.emerald_legacy_launcher" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View file

@ -0,0 +1,4 @@
<resources>
<string name="app_name">LCE Emerald Launcher</string>
<string name="main_activity_title">LCE Emerald Launcher</string>
</resources>

View file

@ -0,0 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.emerald_legacy_launcher" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
<files-path name="files" path="." />
</paths>

View file

@ -0,0 +1,22 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.11.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
tasks.register("clean").configure {
delete("build")
}

View file

@ -0,0 +1,23 @@
plugins {
`kotlin-dsl`
}
gradlePlugin {
plugins {
create("pluginsForCoolKids") {
id = "rust"
implementationClass = "RustPlugin"
}
}
}
repositories {
google()
mavenCentral()
}
dependencies {
compileOnly(gradleApi())
implementation("com.android.tools.build:gradle:8.11.0")
}

View file

@ -0,0 +1,68 @@
import java.io.File
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
open class BuildTask : DefaultTask() {
@Input
var rootDirRel: String? = null
@Input
var target: String? = null
@Input
var release: Boolean? = null
@TaskAction
fun assemble() {
val executable = """npm""";
try {
runTauriCli(executable)
} catch (e: Exception) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
// Try different Windows-specific extensions
val fallbacks = listOf(
"$executable.exe",
"$executable.cmd",
"$executable.bat",
)
var lastException: Exception = e
for (fallback in fallbacks) {
try {
runTauriCli(fallback)
return
} catch (fallbackException: Exception) {
lastException = fallbackException
}
}
throw lastException
} else {
throw e;
}
}
}
fun runTauriCli(executable: String) {
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
val target = target ?: throw GradleException("target cannot be null")
val release = release ?: throw GradleException("release cannot be null")
val args = listOf("run", "--", "tauri", "android", "android-studio-script");
project.exec {
workingDir(File(project.projectDir, rootDirRel))
executable(executable)
args(args)
if (project.logger.isEnabled(LogLevel.DEBUG)) {
args("-vv")
} else if (project.logger.isEnabled(LogLevel.INFO)) {
args("-v")
}
if (release) {
args("--release")
}
args(listOf("--target", target))
}.assertNormalExitValue()
}
}

View file

@ -0,0 +1,85 @@
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.get
const val TASK_GROUP = "rust"
open class Config {
lateinit var rootDirRel: String
}
open class RustPlugin : Plugin<Project> {
private lateinit var config: Config
override fun apply(project: Project) = with(project) {
config = extensions.create("rust", Config::class.java)
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
extensions.configure<ApplicationExtension> {
@Suppress("UnstableApiUsage")
flavorDimensions.add("abi")
productFlavors {
create("universal") {
dimension = "abi"
ndk {
abiFilters += abiList
}
}
defaultArchList.forEachIndexed { index, arch ->
create(arch) {
dimension = "abi"
ndk {
abiFilters.add(defaultAbiList[index])
}
}
}
}
}
afterEvaluate {
for (profile in listOf("debug", "release")) {
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
val buildTask = tasks.maybeCreate(
"rustBuildUniversal$profileCapitalized",
DefaultTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for all targets"
}
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
for (targetPair in targetsList.withIndex()) {
val targetName = targetPair.value
val targetArch = archList[targetPair.index]
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
val targetBuildTask = project.tasks.maybeCreate(
"rustBuild$targetArchCapitalized$profileCapitalized",
BuildTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for $targetArch"
rootDirRel = config.rootDirRel
target = targetName
release = profile == "release"
}
buildTask.dependsOn(targetBuildTask)
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
targetBuildTask
)
}
}
}
}
}

View file

@ -0,0 +1,24 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonFinalResIds=false

View file

@ -0,0 +1,6 @@
#Tue May 10 19:22:52 CST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

185
src-tauri/gen/android/gradlew vendored Executable file
View file

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
src-tauri/gen/android/gradlew.bat vendored Normal file
View file

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -0,0 +1,5 @@
include ':app'
include ':diamondruntime'
project(':diamondruntime').projectDir = new File(settingsDir, '../../../DiamondRuntime/app/app')
apply from: 'tauri.settings.gradle'

View file

@ -1 +1 @@
{"default":{"identifier":"default","description":"Default permissions","local":true,"windows":["main"],"permissions":["core:default","core:app:default","core:event:default","core:image:default","core:menu:default","core:path:default","core:resources:default","core:tray:default","core:webview:default","core:webview:allow-create-webview-window","core:window:default","core:window:allow-set-decorations","core:window:allow-is-fullscreen","core:window:allow-minimize","core:window:allow-toggle-maximize","core:window:allow-close","opener:default","gamepad:default","drpc:default","updater:default",{"identifier":"opener:allow-open-path","allow":[{"path":"**"}]},{"identifier":"opener:allow-reveal-item-in-dir","allow":[{"path":"**"}]},"process:default","deep-link:default","core:window:allow-start-dragging"]},"desktop-capability":{"identifier":"desktop-capability","description":"","local":true,"windows":["main"],"permissions":["updater:default","process:default","deep-link:default","core:window:allow-start-dragging"],"platforms":["macOS","windows","linux"]}}
{"default":{"identifier":"default","description":"Default permissions","local":true,"windows":["main"],"permissions":["core:default","core:app:default","core:event:default","core:image:default","core:menu:default","core:path:default","core:resources:default","core:tray:default","core:webview:default","core:webview:allow-create-webview-window","core:window:default","core:window:allow-set-decorations","core:window:allow-is-fullscreen","core:window:allow-minimize","core:window:allow-toggle-maximize","core:window:allow-close","opener:default","gamepad:default","drpc:default","updater:default",{"identifier":"opener:allow-open-path","allow":[{"path":"**"}]},{"identifier":"opener:allow-reveal-item-in-dir","allow":[{"path":"**"}]},"process:default","deep-link:default","core:window:allow-start-dragging"],"platforms":["windows","linux","macOS"]},"desktop-capability":{"identifier":"desktop-capability","description":"","local":true,"windows":["main"],"permissions":["updater:default","process:default","deep-link:default","core:window:allow-start-dragging"],"platforms":["macOS","windows","linux"]},"mobile-capability":{"identifier":"mobile-capability","description":"Mobile permissions","local":true,"windows":["main"],"permissions":["core:default","core:app:default","core:event:default","core:image:default","core:path:default","core:resources:default","core:webview:default","core:webview:allow-create-webview-window","core:window:default","opener:default",{"identifier":"opener:allow-open-path","allow":[{"path":"**"}]},"gamepad:default","process:default","deep-link:default"],"platforms":["android","iOS"]}}

View file

@ -0,0 +1,104 @@
pub enum BridgeAction {
Play,
OpenContainer,
OpenSettings,
SwitchProton,
InstallDriver,
SetAudioBackend,
}
pub fn launch_bridge(
instance_path: String,
action: BridgeAction,
extra_args: Vec<String>,
) -> Result<(), String> {
#[cfg(target_os = "android")]
{
use jni::objects::{JObject, JValue};
use jni::sys::jint;
use jni::JNIEnv;
use wry::prelude::{dispatch, find_class};
fn start_bridge(
env: &mut JNIEnv,
activity: &JObject,
action: &str,
instance_path: &str,
extra_args: &[String],
) -> jni::errors::Result<()> {
let bridge_class =
find_class(env, activity, "dev.lcehub.emerald.LauncherBridgeActivity".to_string())?;
let intent_class = env.find_class("android/content/Intent")?;
let intent = env.new_object(
intent_class,
"(Landroid/content/Context;Ljava/lang/Class;)V", //neo: i hate smali so much
&[(&activity).into(), (&bridge_class).into()],
)?;
let extra_action = env.new_string("launcher_action")?;
let action_str = env.new_string(action)?;
env.call_method(
&intent,
"putExtra",
"(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;",
&[(&extra_action).into(), (&action_str).into()],
)?;
let extra_path = env.new_string("instance_path")?;
let path_str = env.new_string(instance_path)?;
env.call_method(
&intent,
"putExtra",
"(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;",
&[(&extra_path).into(), (&path_str).into()],
)?;
let extra_args_key = env.new_string("extra_args")?;
let extra_args_json =
env.new_string(&serde_json::to_string(extra_args).unwrap_or_else(|_| "[]".into()))?;
env.call_method(
&intent,
"putExtra",
"(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;",
&[(&extra_args_key).into(), (&extra_args_json).into()],
)?;
env.call_method(
&intent,
"addFlags",
"(I)Landroid/content/Intent;",
&[JValue::Int(0x10000000 as jint)], //neo: FLAG_ACTIVITY_NEW_TASK
)?;
env.call_method(
activity,
"startActivity",
"(Landroid/content/Intent;)V",
&[(&intent).into()],
)?;
Ok(())
}
let action_str = match action {
BridgeAction::Play => "play",
BridgeAction::OpenContainer => "open",
BridgeAction::OpenSettings => "settings",
BridgeAction::SwitchProton => "switch_proton",
BridgeAction::InstallDriver => "install_driver",
BridgeAction::SetAudioBackend => "set_audio_backend",
}
.to_string();
dispatch(move |env, activity, _webview| {
if let Err(e) = start_bridge(env, activity, &action_str, &instance_path, &extra_args) {
eprintln!("[android_bridge] failed to start activity: {e}");
}
});
Ok(())
}
#[cfg(not(target_os = "android"))]
{
let _ = (instance_path, action, extra_args);
Err("Only supported on Android".into())
}
}

View file

@ -33,6 +33,7 @@ pub fn get_external_palettes(app: AppHandle) -> Vec<ThemePalette> {
palettes
}
#[cfg(desktop)]
#[tauri::command]
pub fn import_theme(app: AppHandle) -> Result<String, String> {
let file = rfd::FileDialog::new()
@ -54,6 +55,7 @@ pub fn import_theme(app: AppHandle) -> Result<String, String> {
}
}
#[cfg(desktop)]
#[tauri::command]
pub fn export_settings(app: AppHandle) -> Result<(), String> {
let config = config::load_config_raw(app.clone());
@ -72,6 +74,7 @@ pub fn export_settings(app: AppHandle) -> Result<(), String> {
}
}
#[cfg(desktop)]
#[tauri::command]
pub fn import_settings(app: AppHandle) -> Result<String, String> {
let file = rfd::FileDialog::new()

View file

@ -429,7 +429,7 @@ fn build_empty_chunk_nbt(chunk_x: i32, chunk_z: i32) -> Vec<u8> {
out.extend_from_slice(&0i64.to_be_bytes());
out.extend_from_slice(b"\x01\x00\x10TerrainPopulated\x01");
out.extend_from_slice(b"\x09\x00\x08Entities\x0a\x00\x00\x00\x00");
out.extend_from_slice(b"\x09\x00\x0cTileEntities\x0a\x00\x00\x00\x00");
out.extend_from_slice(b"\x09\x00\x0cTileEntities\x0a\x00\x00\x00\x00"); //neo: someone send help
out.push(0x00);
out.push(0x00);
out

View file

@ -28,7 +28,7 @@ async fn stream_download(
lock.insert(instance_id.to_string(), token);
}
let response = reqwest::get(url).await.map_err(|e| e.to_string())?;
let response = reqwest::Client::new().get(url).header(reqwest::header::USER_AGENT, "Emerald-Launcher").send().await.map_err(|e| e.to_string())?;
if !response.status().is_success() {
{ state.tokens.lock().await.remove(instance_id); }
return Err(format!("Download failed: {}", response.status()));
@ -181,7 +181,18 @@ pub async fn download_and_install(
}
}
#[cfg(not(target_os = "linux"))]
#[cfg(target_os = "android")]
{
let status = std::process::Command::new("unzip")
.args(["-o", zip_path.to_str().unwrap(), "-d", instance_dir.to_str().unwrap()])
.status()
.map_err(|e| e.to_string())?;
if !status.success() {
return Err("Extraction failed".into());
}
}
#[cfg(all(not(target_os = "linux"), not(target_os = "android")))]
{
let mut cmd = std::process::Command::new("tar");
cmd.args(["-xf", zip_path.to_str().unwrap(), "-C", instance_dir.to_str().unwrap()]);
@ -291,6 +302,7 @@ pub async fn check_game_update(
let response = reqwest::Client::new()
.head(&url)
.header(reqwest::header::USER_AGENT, "Emerald-Launcher")
.send()
.await
.map_err(|e| e.to_string())?;

View file

@ -1,4 +1,5 @@
use std::fs;
#[cfg(desktop)]
#[tauri::command]
pub fn pick_folder() -> Result<String, String> {
let folder = rfd::FileDialog::new()
@ -12,6 +13,7 @@ pub fn pick_folder() -> Result<String, String> {
}
}
#[cfg(desktop)]
#[tauri::command]
pub fn pick_file(title: String, filters: Vec<String>) -> Result<String, String> {
let mut dialog = rfd::FileDialog::new().set_title(&title);
@ -26,6 +28,7 @@ pub fn pick_file(title: String, filters: Vec<String>) -> Result<String, String>
}
}
#[cfg(desktop)]
#[tauri::command]
pub fn save_file_dialog(title: String, filename: String, filters: Vec<String>) -> Result<String, String> {
let mut dialog = rfd::FileDialog::new().set_title(&title).set_file_name(&filename);

View file

@ -24,6 +24,52 @@ use crate::workshop_server;
#[tauri::command]
#[allow(non_snake_case)]
pub async fn launch_game(
app: AppHandle,
state: State<'_, GameState>,
instance_id: String,
servers: Vec<McServer>,
mut extra_args: Vec<String>,
) -> Result<(), String> {
extra_args.extend(load_instance_args(&app, &instance_id));
#[cfg(target_os = "android")]
{
let _ = state;
let mut servers = servers;
let working_dir = util::get_instance_working_dir(&app, &instance_id);
if !working_dir.join("Minecraft.Client.exe").exists() {
return Err("Game executable not found in instance folder.".into());
}
let config_val = config::load_config_raw(app.clone());
let lce_online = McServer { name: "LCEOnline Game".into(), ip: "127.0.0.1".into(), port: 61000 };
if !servers.iter().any(|s| s.ip == lce_online.ip && s.port == lce_online.port) {
servers.push(lce_online);
}
if let Some(ref saved) = config_val.saved_servers {
for s in saved {
if !servers.iter().any(|existing| existing.ip == s.ip && existing.port == s.port) {
servers.push(s.clone());
}
}
}
ensure_server_list(&working_dir, servers);
let result = crate::android_runtime::launch_bridge(
working_dir.to_string_lossy().to_string(),
crate::android_runtime::BridgeAction::Play,
extra_args,
);
if result.is_ok() {
playtime::start_session(&app, &instance_id);
}
result
}
#[cfg(not(target_os = "android"))]
launch_game_desktop(app, state, instance_id, servers, extra_args).await
}
#[cfg(not(target_os = "android"))]
async fn launch_game_desktop(
app: AppHandle,
state: State<'_, GameState>,
instance_id: String,
@ -155,9 +201,15 @@ pub async fn launch_game(
let gptk_no_hud = macos::find_executable_recursive(&toolkit_dir, "gameportingtoolkit-no-hud")
.or_else(|| macos::find_executable_recursive(&toolkit_dir, "gameportingtoolkit"));
let wine_binary = macos::find_executable_recursive(&toolkit_dir, "wine64")
.or_else(|| macos::find_executable_recursive(&toolkit_dir, "wine"))
.ok_or_else(|| "Unable to locate wine binary inside runtime.".to_string())?;
let is_intel = std::env::consts::ARCH == "x86_64";
let wine_binary = if is_intel {
macos::find_executable_recursive(&toolkit_dir, "wine")
.or_else(|| macos::find_executable_recursive(&toolkit_dir, "wine64"))
} else {
macos::find_executable_recursive(&toolkit_dir, "wine64")
.or_else(|| macos::find_executable_recursive(&toolkit_dir, "wine"))
}
.ok_or_else(|| "Unable to locate wine binary inside runtime.".to_string())?;
let wine_bin_dir = wine_binary
.parent()
@ -231,7 +283,11 @@ pub async fn launch_game(
return handle_game_exit(&app, &state, result);
}
#[cfg(all(not(target_os = "macos"), not(target_os = "linux")))]
#[cfg(all(
not(target_os = "macos"),
not(target_os = "linux"),
not(target_os = "android")
))]
{
let exe_str = game_exe.to_string_lossy().to_string();
let all_args: Vec<String> = extra_args.clone();
@ -291,8 +347,39 @@ pub fn check_game_installed(app: AppHandle, instance_id: String) -> bool {
#[allow(non_snake_case)]
pub fn open_instance_folder(app: AppHandle, instance_id: String) {
let folder = util::get_instance_working_dir(&app, &instance_id);
if folder.exists() {
let _ = app.opener().open_path(folder.to_str().unwrap(), None::<&str>);
#[cfg(target_os = "android")]
{
let _ = std::fs::create_dir_all(&folder);
let _ = crate::android_runtime::launch_bridge(
folder.to_string_lossy().to_string(),
crate::android_runtime::BridgeAction::OpenContainer,
Vec::new(),
);
}
#[cfg(not(target_os = "android"))]
{
if folder.exists() {
let _ = app.opener().open_path(folder.to_str().unwrap(), None::<&str>);
}
}
}
#[tauri::command]
#[allow(non_snake_case)]
pub fn open_container_settings(app: AppHandle, instance_id: String) -> Result<(), String> {
#[cfg(target_os = "android")]
{
let folder = util::get_instance_working_dir(&app, &instance_id);
crate::android_runtime::launch_bridge(
folder.to_string_lossy().to_string(),
crate::android_runtime::BridgeAction::OpenSettings,
Vec::new(),
)
}
#[cfg(not(target_os = "android"))]
{
let _ = (app, instance_id);
Err("Only supported on Android".into())
}
}
@ -331,6 +418,21 @@ pub fn get_instance_path(app: AppHandle, instance_id: String) -> String {
.to_string()
}
fn load_instance_args(app: &AppHandle, instance_id: &str) -> Vec<String> {
let config_val = config::load_config_raw(app.clone());
config_val
.instance_launch_args
.and_then(|m| m.get(instance_id).cloned())
.map(|entry| entry.args)
.unwrap_or_default()
}
#[tauri::command]
pub fn get_instance_args_schema(app: AppHandle, instance_id: String) -> Option<String> {
let dir = util::get_instance_working_dir(&app, &instance_id);
fs::read_to_string(dir.join("Arguments.Schema.json")).ok()
}
#[tauri::command]
pub fn get_playtime(app: AppHandle, instance_id: String) -> PlaytimeResponse {
playtime::get_playtime(&app, &instance_id)
@ -341,6 +443,7 @@ pub fn get_playtime_daily(app: AppHandle, instance_id: String, days: u64) -> Vec
playtime::get_playtime_daily(&app, &instance_id, days)
}
#[cfg(desktop)]
#[tauri::command]
pub fn backup_instance(app: AppHandle, instance_id: String) -> Result<(), String> {
let instance_dir = util::get_instance_working_dir(&app, &instance_id);
@ -374,6 +477,7 @@ pub fn backup_instance(app: AppHandle, instance_id: String) -> Result<(), String
}
}
#[cfg(desktop)]
#[tauri::command]
pub fn restore_instance(app: AppHandle) -> Result<String, String> {
let file = rfd::FileDialog::new()
@ -568,6 +672,7 @@ fn apply_launch_env_vars(cmd: &mut tokio::process::Command, config: &AppConfig)
const MAX_LOG_BYTES: usize = 1024 * 1024;
struct GameRunResult {
log: String,
exit_code: i32,
}
fn spawn_log_reader<R>(mut reader: R, log: Arc<Mutex<Vec<u8>>>) -> tokio::task::JoinHandle<()> where R: AsyncRead + Unpin + Send + 'static, {
@ -607,11 +712,13 @@ async fn run_game_and_capture(
let mut lock = state.child.lock().await;
*lock = Some(child);
}
let exit_code;
loop {
{
let mut lock = state.child.lock().await;
if let Some(ref mut c) = *lock {
if c.try_wait().map_err(|e| e.to_string())?.is_some() {
if let Some(status) = c.try_wait().map_err(|e| e.to_string())? {
exit_code = status.code().unwrap_or(1);
break;
}
} else {
@ -629,14 +736,21 @@ async fn run_game_and_capture(
}
let bytes = log.lock().await.clone();
let log_str = String::from_utf8_lossy(&bytes).to_string();
Ok(Some(GameRunResult { log: log_str }))
Ok(Some(GameRunResult { log: log_str, exit_code }))
}
fn game_exited_ok(log: &str) -> bool {
log.lines()
.rev()
.take(3)
.any(|line| line.contains("AppPolicyGetProcessTerminationMethod"))
fn game_exited_ok(result: &GameRunResult) -> bool {
#[cfg(not(target_os = "windows"))]
{
result.log.lines()
.rev()
.take(3)
.any(|line| line.contains("AppPolicyGetProcessTerminationMethod"))
}
#[cfg(target_os = "windows")]
{
result.exit_code == 0
}
}
fn handle_game_exit(
@ -644,7 +758,7 @@ fn handle_game_exit(
state: &State<'_, GameState>,
result: GameRunResult,
) -> Result<(), String> {
if state.manual_stop.swap(false, Ordering::SeqCst) || game_exited_ok(&result.log) {
if state.manual_stop.swap(false, Ordering::SeqCst) || game_exited_ok(&result) {
return Ok(());
}
let _ = app.emit("game-log", result.log);
@ -669,3 +783,63 @@ async fn perform_instance_sync(app: &AppHandle, instance_id: &str) -> Result<(),
perform_dlc_sync(app, &target_dir)?;
Ok(())
}
#[tauri::command]
#[cfg(target_os = "android")]
pub fn switch_proton(app: AppHandle, version: String) -> Result<(), String> {
let instance_id = {
let config_val = config::load_config_raw(app.clone());
config_val.profile.unwrap_or_else(|| "legacy_evolved".into())
};
crate::android_runtime::launch_bridge(
instance_id,
crate::android_runtime::BridgeAction::SwitchProton,
vec![version],
)
}
#[tauri::command]
#[cfg(not(target_os = "android"))]
pub fn switch_proton(_app: AppHandle, _version: String) -> Result<(), String> {
Err("Only supported on Android".into())
}
#[tauri::command]
#[cfg(target_os = "android")]
pub fn install_latest_driver(app: AppHandle) -> Result<(), String> {
let instance_id = {
let config_val = config::load_config_raw(app.clone());
config_val.profile.unwrap_or_else(|| "legacy_evolved".into())
};
crate::android_runtime::launch_bridge(
instance_id,
crate::android_runtime::BridgeAction::InstallDriver,
Vec::new(),
)
}
#[tauri::command]
#[cfg(not(target_os = "android"))]
pub fn install_latest_driver(_app: AppHandle) -> Result<(), String> {
Err("Only supported on Android".into())
}
#[tauri::command]
#[cfg(target_os = "android")]
pub fn set_audio_backend(app: AppHandle, backend: String) -> Result<(), String> {
let instance_id = {
let config_val = config::load_config_raw(app.clone());
config_val.profile.unwrap_or_else(|| "legacy_evolved".into())
};
crate::android_runtime::launch_bridge(
instance_id,
crate::android_runtime::BridgeAction::SetAudioBackend,
vec![backend],
)
}
#[tauri::command]
#[cfg(not(target_os = "android"))]
pub fn set_audio_backend(_app: AppHandle, _backend: String) -> Result<(), String> {
Err("Only supported on Android".into())
}

View file

@ -1285,6 +1285,7 @@ static MODERN_DIRECT_MAP: once_cell::sync::Lazy<HashMap<&'static str, LegacyBloc
"lava_cauldron",
LegacyBlockState { id: 118, data: 0 },
);
//neo: m.insert("help", neo {thisis: verytiring});
m
});

View file

@ -21,7 +21,7 @@ pub enum NbtValue {
#[derive(Debug, Clone, Default, PartialEq)]
pub struct NbtCompound {
pub name: String,
pub tags: Vec<(String, NbtValue)>,
pub tags: Vec<(String, NbtValue)>, //neo: if you came here asking about this, dont. please. dont ask why im storing a tuple in a vec. or why im manually resizing a vec.
}
impl NbtCompound {

View file

@ -1,3 +1,4 @@
//neo: thanks Huckle for the Intel macOS support reference code lol
use tauri::AppHandle;
#[cfg(target_os = "macos")]
use std::io::Write;
@ -55,6 +56,14 @@ pub async fn setup_macos_runtime(window: tauri::Window, app: AppHandle) -> Resul
assets: Vec<GithubAsset>,
}
let is_intel = std::env::consts::ARCH == "x86_64";
let repo = if is_intel {
"Gcenx/macOS_Wine_builds"
} else {
"Gcenx/game-porting-toolkit"
};
macos::emit_macos_setup_progress(
&window,
"resolving",
@ -64,7 +73,7 @@ pub async fn setup_macos_runtime(window: tauri::Window, app: AppHandle) -> Resul
let client = reqwest::Client::new();
let release_text = client
.get("https://api.github.com/repos/Gcenx/game-porting-toolkit/releases/latest")
.get(format!("https://api.github.com/repos/{}/releases/latest", repo))
.header("User-Agent", "Emerald-Legacy-Launcher")
.send()
.await
@ -80,7 +89,15 @@ pub async fn setup_macos_runtime(window: tauri::Window, app: AppHandle) -> Resul
let asset = release
.assets
.iter()
.find(|a| a.name.ends_with(".tar.xz") || a.name.ends_with(".tar.gz"))
.find(|a| {
let is_archive =
a.name.ends_with(".tar.xz") || a.name.ends_with(".tar.gz");
if is_intel {
is_archive && a.name.contains("staging")
} else {
is_archive
}
})
.ok_or_else(|| "No compatible runtime asset found in latest release.".to_string())?;
let runtime_dir = macos::get_macos_runtime_dir(&app);
@ -171,9 +188,14 @@ pub async fn setup_macos_runtime(window: tauri::Window, app: AppHandle) -> Resul
}
fs::create_dir_all(&prefix_dir).map_err(|e| e.to_string())?;
let wine_binary = macos::find_executable_recursive(&toolkit_dir, "wine64")
.or_else(|| macos::find_executable_recursive(&toolkit_dir, "wine"))
.ok_or_else(|| "Unable to locate wine binary inside runtime.".to_string())?;
let wine_binary = if is_intel {
macos::find_executable_recursive(&toolkit_dir, "wine")
.or_else(|| macos::find_executable_recursive(&toolkit_dir, "wine64"))
} else {
macos::find_executable_recursive(&toolkit_dir, "wine64")
.or_else(|| macos::find_executable_recursive(&toolkit_dir, "wine"))
}
.ok_or_else(|| "Unable to locate wine binary inside runtime.".to_string())?;
let wine_bin_dir = wine_binary
.parent()
@ -213,6 +235,181 @@ pub async fn setup_macos_runtime(window: tauri::Window, app: AppHandle) -> Resul
return Err("Wine prefix initialization failed".into());
}
if is_intel {
macos::emit_macos_setup_progress(
&window,
"dxvk",
"Fetching latest DXVK…".into(),
None,
);
#[derive(Deserialize)]
struct DxvkAsset {
name: String,
browser_download_url: String,
}
#[derive(Deserialize)]
struct DxvkRelease {
assets: Vec<DxvkAsset>,
}
let dxvk_release_text = client
.get("https://api.github.com/repos/Gcenx/DXVK-macOS/releases/latest")
.header("User-Agent", "Emerald-Legacy-Launcher")
.send()
.await
.map_err(|e| e.to_string())?
.error_for_status()
.map_err(|e| e.to_string())?
.text()
.await
.map_err(|e| e.to_string())?;
let dxvk_release: DxvkRelease =
serde_json::from_str(&dxvk_release_text).map_err(|e| e.to_string())?;
let dxvk_asset = dxvk_release
.assets
.iter()
.find(|a| {
let is_archive =
a.name.ends_with(".tar.xz") || a.name.ends_with(".tar.gz");
is_archive && !a.name.contains("builtin")
})
.ok_or_else(|| "No compatible DXVK asset found.".to_string())?;
macos::emit_macos_setup_progress(
&window,
"dxvk_downloading",
"Downloading DXVK…".into(),
Some(0.0),
);
let dxvk_archive_path = runtime_dir.join(format!("dxvk_{}", dxvk_asset.name));
let dxvk_response = client
.get(&dxvk_asset.browser_download_url)
.header("User-Agent", "Emerald-Legacy-Launcher")
.send()
.await
.map_err(|e| e.to_string())?
.error_for_status()
.map_err(|e| e.to_string())?;
let dxvk_total = dxvk_response.content_length().unwrap_or(0) as f64;
let mut dxvk_file = fs::File::create(&dxvk_archive_path).map_err(|e| e.to_string())?;
let mut dxvk_downloaded = 0.0;
let mut dxvk_last_percent: i64 = -1;
let mut dxvk_stream = dxvk_response.bytes_stream();
while let Some(chunk) = dxvk_stream.next().await {
let chunk = chunk.map_err(|e| e.to_string())?;
dxvk_file.write_all(&chunk).map_err(|e| e.to_string())?;
dxvk_downloaded += chunk.len() as f64;
if dxvk_total > 0.0 {
let percent = (dxvk_downloaded / dxvk_total * 100.0).clamp(0.0, 100.0);
let rounded = percent.floor() as i64;
if rounded != dxvk_last_percent {
dxvk_last_percent = rounded;
macos::emit_macos_setup_progress(
&window,
"dxvk_downloading",
format!("Downloading DXVK… {}%", rounded),
Some(percent),
);
}
}
}
drop(dxvk_file);
macos::emit_macos_setup_progress(
&window,
"dxvk_extracting",
"Extracting DXVK…".into(),
None,
);
let dxvk_dir = runtime_dir.join("dxvk");
fs::create_dir_all(&dxvk_dir).map_err(|e| e.to_string())?;
let status = Command::new("tar")
.args([
"-xf",
dxvk_archive_path
.to_str()
.ok_or_else(|| "Invalid DXVK archive path".to_string())?,
"-C",
dxvk_dir
.to_str()
.ok_or_else(|| "Invalid DXVK extraction path".to_string())?,
])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.status()
.map_err(|e| e.to_string())?;
let _ = fs::remove_file(&dxvk_archive_path);
if !status.success() {
return Err(format!("DXVK extraction failed with status: {:?}", status));
}
macos::emit_macos_setup_progress(
&window,
"dxvk_patching",
"Patching runtime with DXVK…".into(),
None,
);
let system32 = prefix_dir.join("drive_c/windows/system32");
let syswow64 = prefix_dir.join("drive_c/windows/syswow64");
let _ = fs::create_dir_all(&system32);
let _ = fs::create_dir_all(&syswow64);
let copy_dir = |src_dir: &std::path::Path, dst_dir: &std::path::Path| -> Result<(), String> {
if !src_dir.exists() {
return Ok(());
}
for entry in fs::read_dir(src_dir).map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("dll") {
let dest = dst_dir.join(path.file_name().unwrap());
fs::copy(&path, &dest).map_err(|e| e.to_string())?;
}
}
Ok(())
};
let dxvk_x64 = dxvk_dir.join("x64");
let dxvk_x32 = dxvk_dir.join("x32");
copy_dir(&dxvk_x64, &system32)?;
copy_dir(&dxvk_x32, &syswow64)?;
for dll_name in &["d3d11", "dxgi"] {
let mut reg_cmd = Command::new(&wine_binary);
reg_cmd
.args(["reg", "add", "HKCU\\Software\\Wine\\DllOverrides"])
.arg("/v")
.arg(dll_name)
.arg("/t")
.arg("REG_SZ")
.arg("/d")
.arg("native,builtin")
.arg("/f");
reg_cmd
.env("WINEPREFIX", &prefix_dir)
.env("WINEDEBUG", "-all")
.env(
"PATH",
format!(
"{}:{}",
wine_bin_dir.to_string_lossy(),
std::env::var("PATH").unwrap_or_default()
),
)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
let _ = reg_cmd.status();
}
let _ = fs::remove_dir_all(&dxvk_dir);
}
macos::emit_macos_setup_progress(&window, "done", "Setup complete.".into(), Some(100.0));
Ok(())
}

View file

@ -15,7 +15,7 @@ pub async fn add_to_steam(
let (exe_str, launch_options, start_dir) = if in_flatpak {
(
"/usr/bin/flatpak".to_string(),
format!("run io.github.Emerald_Legacy_Launcher.Emerald_Legacy_Launcher \"{}\"", instance_id),
format!("run io.github.Emerald_Legacy_Launcher.Emerald_Legacy_Launcher \"{}\"", instance_id), //neo: yes i'm hardcoding it lmfao
std::env::var("HOME").unwrap_or_default(),
)
} else {

View file

@ -152,6 +152,32 @@ fn extract_archive(zip_tmp: &Path, dest_dir: &Path) -> Result<(Vec<String>, bool
extract_ok = st.success();
}
Ok((files, extract_ok))
} else if cfg!(target_os = "android") {
let unzip_list = std::process::Command::new("unzip")
.args(["-l", zip_str])
.output()
.map_err(|e| e.to_string())?;
if !unzip_list.status.success() {
return Err(format!("Failed to list contents of {}", zip_str));
}
let listing = String::from_utf8_lossy(&unzip_list.stdout);
let files: Vec<String> = listing.lines()
.filter_map(|l| {
let mut parts = l.trim().split_whitespace();
let size_str = parts.next()?;
size_str.parse::<u64>().ok()?;
parts.next()?;
parts.next()?;
Some(parts.collect::<Vec<&str>>().join(" "))
})
.filter(|l| !l.ends_with('/') && !l.contains('*'))
.map(|l| dest_dir.join(l).to_string_lossy().to_string())
.collect();
let st = std::process::Command::new("unzip")
.args(["-o", zip_str, "-d", dest_dir.to_str().unwrap()])
.status()
.map_err(|e| e.to_string())?;
Ok((files, st.success()))
} else {
let st = std::process::Command::new("tar")
.args(["-xf", zip_str, "-C", dest_dir.to_str().unwrap()])

View file

@ -37,6 +37,9 @@ pub fn load_config_raw(app: AppHandle) -> AppConfig {
extra_launch_args: None,
launch_prefix: None,
launch_env_vars: None,
instance_launch_args: None,
android_runner: None,
android_audio_backend: None,
}
}

35
src-tauri/src/lce_auth.rs Normal file
View file

@ -0,0 +1,35 @@
use tauri::plugin::{Builder, PluginHandle, TauriPlugin};
use tauri::Wry;
pub struct LceAuthState(pub PluginHandle<Wry>);
pub fn init() -> TauriPlugin<Wry> {
Builder::new("emerald-lce-auth")
.setup(|app, api| {
#[cfg(target_os = "android")]
{
use tauri::Manager;
let handle = api.register_android_plugin("com.emerald.legacy", "LceAuthPlugin")?;
app.manage(LceAuthState(handle));
}
Ok(())
})
.build()
}
#[tauri::command]
pub async fn start_lce_auth(app: tauri::AppHandle) -> Result<String, String> {
#[cfg(target_os = "android")]
{
use tauri::Manager;
let state = app.state::<LceAuthState>();
state
.0
.run_mobile_plugin_async("startAuth", ())
.await
.map_err(|e| e.to_string())
}
#[cfg(not(target_os = "android"))]
{
let _ = app;
Err("LCE Online auth is only supported on Android".into()) //neo: erm, rust-side only. its supported on desktop on the typescript side.
}
}

View file

@ -1,3 +1,4 @@
#![cfg_attr(target_os = "android", allow(dead_code, unused))]
mod types;
mod state;
mod config;
@ -6,6 +7,9 @@ mod playtime;
mod platform;
mod networking;
mod workshop_server;
mod lce_auth;
#[cfg(target_os = "android")]
mod android_runtime;
mod commands;
use std::collections::HashMap;
use std::sync::Arc;
@ -30,25 +34,34 @@ use state::{DownloadState, GameState, ProxyGuard};
fn webview_deep_link_interceptor() -> impl tauri::plugin::Plugin<tauri::Wry> { tauri::plugin::Builder::<tauri::Wry>::new("emerald-deep-link-interceptor").on_navigation(|webview, url| { if url.scheme() == "emerald" || url.scheme() == "emeraldlauncher" { let _ = webview.app_handle().emit("deep-link", vec![url.to_string()]); false } else { true }}).build()}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_deep_link::init())
.plugin(webview_deep_link_interceptor())
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
let urls: Vec<String> = args
.iter()
.filter(|a| {
a.starts_with("emerald://")
|| a.starts_with("emeraldlauncher://")
|| a.starts_with("discord-1482504445152460871://")
})
.cloned()
.collect();
if !urls.is_empty() {
let _ = app.emit("deep-link", urls);
}
}))
.plugin(lce_auth::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_gamepad::init())
.plugin(tauri_plugin_opener::init());
#[cfg(desktop)]
{
builder = builder
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
let urls: Vec<String> = args
.iter()
.filter(|a| {
a.starts_with("emerald://")
|| a.starts_with("emeraldlauncher://")
|| a.starts_with("discord-1482504445152460871://")
})
.cloned()
.collect();
if !urls.is_empty() {
let _ = app.emit("deep-link", urls);
}
}))
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_drpc::init());
}
builder
.manage(DownloadState {
tokens: Arc::new(Mutex::new(HashMap::new())),
})
@ -61,9 +74,6 @@ pub fn run() {
cancel_tokens: Arc::new(Mutex::new(HashMap::new())),
local_port: Arc::new(Mutex::new(None)),
})
.plugin(tauri_plugin_gamepad::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_drpc::init())
.invoke_handler(tauri::generate_handler![
macos_setup::setup_macos_runtime,
dlc::list_git_directory,
@ -75,12 +85,17 @@ pub fn run() {
config_cmds::load_config,
download::download_and_install,
game::open_instance_folder,
game::open_container_settings,
download::cancel_download,
runners::get_available_runners,
config_cmds::get_external_palettes,
#[cfg(desktop)]
config_cmds::import_theme,
#[cfg(desktop)]
config_cmds::export_settings,
#[cfg(desktop)]
config_cmds::import_settings,
#[cfg(desktop)]
file_dialogs::pick_folder,
download::download_runner,
game::delete_instance,
@ -97,7 +112,9 @@ pub fn run() {
macos_setup::check_macos_runtime_installed,
macos_setup::check_macos_runtime_installed_fast,
skin::download_logo,
#[cfg(desktop)]
file_dialogs::pick_file,
#[cfg(desktop)]
file_dialogs::save_file_dialog,
file_dialogs::write_binary_file,
file_dialogs::read_binary_file,
@ -107,7 +124,10 @@ pub fn run() {
game::get_instance_path,
game::get_playtime,
game::get_playtime_daily,
game::get_instance_args_schema,
#[cfg(desktop)]
game::backup_instance,
#[cfg(desktop)]
game::restore_instance,
commands::console2lce::import_world,
commands::console2lce::import_lce_save,
@ -119,45 +139,67 @@ pub fn run() {
relay::stop_proxy,
relay::stop_all_proxies,
relay::join_game,
lce_auth::start_lce_auth,
plugins::get_plugins_dir,
plugins::list_directory,
plugins::create_plugin_dir,
plugins::remove_plugin_dir,
game::switch_proton,
game::install_latest_driver,
game::set_audio_backend,
])
.setup(|app| {
let app_handle = app.handle().clone();
let config = config::load_config_raw(app_handle.clone());
if config.start_fullscreen.unwrap_or(false) {
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.set_fullscreen(true);
#[cfg(desktop)]
{
let config = config::load_config_raw(app_handle.clone());
if config.start_fullscreen.unwrap_or(false) {
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.set_fullscreen(true);
}
}
}
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 && !args[1].starts_with('-') {
let first = &args[1];
let is_deep_link = first.starts_with("emerald://")
|| first.starts_with("emeraldlauncher://")
|| first.starts_with("discord-1482504445152460871://");
if !is_deep_link {
let instance_id = first.clone();
let app_handle_clone = app.handle().clone();
tauri::async_runtime::spawn(async move {
if let Some(window) = app_handle_clone.get_webview_window("main") {
let _ = window.hide();
}
let state = app_handle_clone.state::<GameState>();
match game::launch_game(app_handle_clone.clone(), state, instance_id, Vec::new(), vec![]).await {
Ok(_) => app_handle_clone.exit(0),
Err(e) => {
let _ = app_handle_clone.emit("backend-error", format!("Auto-launch: {e}"));
eprintln!("Auto-launch error: {}", e);
app_handle_clone.exit(1);
}
#[cfg(target_os = "android")]
{
let handle = app_handle.clone();
if let Some(window) = app_handle.get_webview_window("main") {
window.on_window_event(move |event| {
if let tauri::WindowEvent::Focused(true) = event {
playtime::finish_active_session(&handle);
}
});
}
}
#[cfg(desktop)]
{
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 && !args[1].starts_with('-') {
let first = &args[1];
let is_deep_link = first.starts_with("emerald://")
|| first.starts_with("emeraldlauncher://")
|| first.starts_with("discord-1482504445152460871://");
if !is_deep_link {
let instance_id = first.clone();
let app_handle_clone = app.handle().clone();
tauri::async_runtime::spawn(async move {
if let Some(window) = app_handle_clone.get_webview_window("main") {
let _ = window.hide();
}
let state = app_handle_clone.state::<GameState>();
match game::launch_game(app_handle_clone.clone(), state, instance_id, Vec::new(), vec![]).await {
Ok(_) => app_handle_clone.exit(0),
Err(e) => {
let _ = app_handle_clone.emit("backend-error", format!("Auto-launch: {e}"));
eprintln!("Auto-launch error: {}", e);
app_handle_clone.exit(1);
}
}
});
}
}
}
Ok(())
})
.run(tauri::generate_context!())

View file

@ -50,6 +50,9 @@ pub fn is_macos_runtime_installed(app: &AppHandle) -> bool {
if !toolkit_dir.exists() {
return false;
}
let candidates = ["Game Porting Toolkit.app"];
candidates.iter().any(|name| find_executable_recursive(&toolkit_dir, name).is_some())
if find_executable_recursive(&toolkit_dir, "Game Porting Toolkit.app").is_some() {
return true;
}
find_executable_recursive(&toolkit_dir, "wine").is_some()
|| find_executable_recursive(&toolkit_dir, "wine64").is_some()
}

View file

@ -15,6 +15,12 @@ pub struct PlaytimeData {
pub sessions: HashMap<String, Vec<PlaytimeSession>>,
}
#[derive(Serialize, Deserialize)]
struct ActiveSession {
instance_id: String,
start: u64,
}
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PlaytimeResponse {
@ -54,6 +60,34 @@ pub fn record_session(app: &AppHandle, instance_id: &str, start: u64, end: u64)
save(app, &data);
}
const MAX_SESSION_SECONDS: u64 = 12 * 60 * 60;
fn active_session_path(app: &AppHandle) -> PathBuf {
util::get_app_dir(app).join("active_session.json")
}
pub fn start_session(app: &AppHandle, instance_id: &str) {
let start = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let active = ActiveSession { instance_id: instance_id.to_string(), start };
let path = active_session_path(app);
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Ok(content) = serde_json::to_string(&active) {
let _ = std::fs::write(&path, content);
}
}
pub fn finish_active_session(app: &AppHandle) {
let path = active_session_path(app);
let Ok(content) = std::fs::read_to_string(&path) else { return };
let _ = std::fs::remove_file(&path);
let Ok(active) = serde_json::from_str::<ActiveSession>(&content) else { return };
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
if now > active.start && now - active.start <= MAX_SESSION_SECONDS {
record_session(app, &active.instance_id, active.start, now);
}
}
pub fn get_playtime(app: &AppHandle, instance_id: &str) -> PlaytimeResponse {
let data = load(app);
let sessions = data.sessions.get(instance_id);

View file

@ -32,6 +32,13 @@ pub struct CustomizationEntry {
pub panorama: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct InstanceLaunchArgs {
pub values: std::collections::HashMap<String, serde_json::Value>,
pub args: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AppConfig {
@ -59,6 +66,9 @@ pub struct AppConfig {
pub extra_launch_args: Option<Vec<String>>,
pub launch_prefix: Option<String>,
pub launch_env_vars: Option<std::collections::HashMap<String, String>>,
pub instance_launch_args: Option<std::collections::HashMap<String, InstanceLaunchArgs>>,
pub android_runner: Option<String>,
pub android_audio_backend: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]

View file

@ -3,7 +3,7 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;
use tokio_util::sync::CancellationToken;
use tauri::{AppHandle, Emitter};
const REGISTRY_URL: &str = "https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main";
const REGISTRY_URL: &str = "https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main"; //neo: more hardcoding!
static CLIENT: Lazy<reqwest::Client> = Lazy::new(|| reqwest::Client::new());
pub struct Guard {
cancel: Option<CancellationToken>,
@ -34,7 +34,7 @@ pub async fn start(app: AppHandle) -> CancellationToken {
}
async fn serve(app: AppHandle, cancel: CancellationToken) {
let listener = match TcpListener::bind("127.0.0.1:5582").await {
let listener = match TcpListener::bind("127.0.0.1:5582").await { //neo: dont say a word about this weird port.
Ok(l) => l,
Err(e) => {
let _ = app.emit("backend-error", format!("Workshop server failed to bind: {e}"));

View file

@ -10,6 +10,15 @@
"frontendDist": "../dist"
},
"app": {
"security": {
"assetProtocol": {
"enable": true,
"scope": {
"allow": ["**"],
"requireLiteralLeadingDot": false
}
}
},
"windows": [
{
"label": "main",
@ -65,6 +74,10 @@
"frameworks": [],
"minimumSystemVersion": "10.15",
"signingIdentity": null
},
"android": {
"minSdkVersion": 26,
"versionCode": 1006000
}
}
}

View file

@ -144,13 +144,37 @@ const CapePreview = memo(function CapePreview({
render();
}
};
const onTouchStart = (e: TouchEvent) => {
const t = e.touches[0];
if (!t) return;
isDragging = true;
previousMousePosition = { x: t.clientX, y: t.clientY };
};
const onTouchMove = (e: TouchEvent) => {
const t = e.touches[0];
if (isDragging && groupRef.current && t) {
e.preventDefault();
groupRef.current.rotation.y +=
(t.clientX - previousMousePosition.x) * 0.01;
previousMousePosition = { x: t.clientX, y: t.clientY };
render();
}
};
const onTouchEnd = () => {
isDragging = false;
};
renderer.domElement.addEventListener("mousedown", onMouseDown);
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
renderer.domElement.addEventListener("touchstart", onTouchStart, { passive: true });
window.addEventListener("touchmove", onTouchMove, { passive: false });
window.addEventListener("touchend", onTouchEnd);
return () => {
active = false;
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
window.removeEventListener("touchmove", onTouchMove);
window.removeEventListener("touchend", onTouchEnd);
scene.traverse((object) => {
if (object instanceof THREE.Mesh) {
if (object.geometry) object.geometry.dispose();

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from "react";
import { TauriService } from "../../services/TauriService";
import { useState } from "react";
import { convertFileSrc } from "@tauri-apps/api/core";
interface ScreenshotImageProps {
path: string;
className?: string;
@ -9,50 +9,6 @@ interface ScreenshotImageProps {
fallbackSrc?: string;
}
const imgCache = new Map<string, string>();
let activeLoads = 0;
const MAX_CONCURRENT = 4;
const loadQueue: Array<() => void> = [];
function dequeue() {
while (activeLoads < MAX_CONCURRENT && loadQueue.length > 0) {
const next = loadQueue.shift()!;
activeLoads++;
next();
}
}
function enqueueLoad(
path: string,
onLoad: (url: string) => void,
onError: () => void,
) {
const cached = imgCache.get(path);
if (cached) {
onLoad(cached);
return;
}
const run = () => {
TauriService.readScreenshotAsDataUrl(path)
.then((url) => {
imgCache.set(path, url);
onLoad(url);
})
.catch(() => onError())
.finally(() => {
activeLoads--;
dequeue();
});
};
if (activeLoads < MAX_CONCURRENT) {
activeLoads++;
run();
} else {
loadQueue.push(run);
}
}
export function ScreenshotImage({
path,
className,
@ -61,62 +17,18 @@ export function ScreenshotImage({
style,
fallbackSrc,
}: ScreenshotImageProps) {
const [src, setSrc] = useState<string | undefined>(fallbackSrc);
const imgRef = useRef<HTMLImageElement>(null);
const loadedRef = useRef(false);
useEffect(() => {
const el = imgRef.current?.parentElement || imgRef.current;
if (!el) return;
let cancelled = false;
const doLoad = () => {
if (loadedRef.current) return;
loadedRef.current = true;
enqueueLoad(
path,
(url) => {
if (!cancelled) setSrc(url);
},
() => {
if (!cancelled && fallbackSrc) setSrc(fallbackSrc);
},
);
};
if (loading === "eager") {
doLoad();
return () => {
cancelled = true;
};
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
observer.disconnect();
doLoad();
}
},
{ rootMargin: "800px" },
);
observer.observe(el);
return () => {
cancelled = true;
observer.disconnect();
};
}, [path, fallbackSrc, loading]);
const handleError = () => {
if (fallbackSrc) setSrc(fallbackSrc);
};
const [hasError, setHasError] = useState(false);
return (
<img
ref={imgRef}
src={src}
src={hasError && fallbackSrc ? fallbackSrc : convertFileSrc(path)}
className={className}
alt={alt}
loading={loading}
style={style}
onError={handleError}
onError={() => {
if (fallbackSrc) setHasError(true);
}}
/>
);
}

View file

@ -16,6 +16,7 @@ interface SkinViewerProps {
onNavigateRight: () => void;
hideControls?: boolean;
style?: React.CSSProperties;
slim?: boolean;
}
const SkinViewer = memo(function SkinViewer({
@ -30,6 +31,7 @@ const SkinViewer = memo(function SkinViewer({
onNavigateRight,
hideControls,
style,
slim,
}: SkinViewerProps) {
const mountRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
@ -181,17 +183,19 @@ const SkinViewer = memo(function SkinViewer({
});
const isSlim =
!isLegacy &&
(() => {
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
if (!ctx) return false;
ctx.drawImage(img, 0, 0);
const data = ctx.getImageData(42, 48, 1, 1).data;
return data[3] === 0;
})();
slim !== undefined
? slim
: !isLegacy &&
(() => {
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
if (!ctx) return false;
ctx.drawImage(img, 0, 0);
const data = ctx.getImageData(42, 48, 1, 1).data;
return data[3] === 0;
})();
const armW = isSlim ? 3 : 4;
const headUv = {
top: [8, 0, 8, 8],
@ -379,7 +383,7 @@ const SkinViewer = memo(function SkinViewer({
}
const name = username.toLowerCase();
playerGroup.rotation.y = -0.3;
playerGroup.rotation.y = 0.3;
if (name === "dinnerbone" || name === "grumm") {
playerGroup.scale.y = -1;
playerGroup.position.y = 1.5;
@ -404,15 +408,39 @@ const SkinViewer = memo(function SkinViewer({
requestRenderRef.current?.();
}
};
const onTouchStart = (e: TouchEvent) => {
const t = e.touches[0];
if (!t) return;
isDragging = true;
previousMousePosition = { x: t.clientX, y: t.clientY };
};
const onTouchMove = (e: TouchEvent) => {
const t = e.touches[0];
if (isDragging && t) {
e.preventDefault();
const delta = (t.clientX - previousMousePosition.x) * 0.01;
playerGroup.rotation.y += delta;
previousMousePosition = { x: t.clientX, y: t.clientY };
requestRenderRef.current?.();
}
};
const onTouchEnd = () => {
isDragging = false;
};
requestRenderRef.current = () => renderer.render(scene, camera);
requestRenderRef.current();
renderer.domElement.addEventListener("mousedown", onMouseDown);
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
renderer.domElement.addEventListener("touchstart", onTouchStart, { passive: true });
window.addEventListener("touchmove", onTouchMove, { passive: false });
window.addEventListener("touchend", onTouchEnd);
return () => {
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
window.removeEventListener("touchmove", onTouchMove);
window.removeEventListener("touchend", onTouchEnd);
scene.traverse((object) => {
if (object instanceof THREE.Mesh) {
if (object.geometry) object.geometry.dispose();
@ -437,7 +465,7 @@ const SkinViewer = memo(function SkinViewer({
easterEggRef.current = null;
requestRenderRef.current = null;
};
}, [skinUrl, capeUrl]);
}, [skinUrl, capeUrl, slim]);
useEffect(() => {
const group = playerGroupRef.current;

Some files were not shown because too many files have changed in this diff Show more