Merge remote-tracking branch 'upstream/release-9.x' into develop

This commit is contained in:
Evan Goode 2025-04-06 18:23:05 -04:00 committed by Luna
parent 8940008b41
commit 48689d1b79
63 changed files with 845 additions and 697 deletions

View File

@ -39,9 +39,6 @@ on:
APPLE_NOTARIZE_PASSWORD:
description: Password used for notarizing macOS builds
required: false
CACHIX_AUTH_TOKEN:
description: Private token for authenticating against Cachix cache
required: false
GPG_PRIVATE_KEY:
description: Private key for AppImage signing
required: false
@ -68,6 +65,9 @@ jobs:
qt_arch: ""
qt_version: "6.5.3"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
linuxdeploy_hash: "4648f278ab3ef31f819e67c30d50f462640e5365a77637d7e6f2ad9fd0b4522a linuxdeploy-x86_64.AppImage"
linuxdeploy_qt_hash: "15106be885c1c48a021198e7e1e9a48ce9d02a86dd0a1848f00bdbf3c1c92724 linuxdeploy-plugin-qt-x86_64.AppImage"
appimageupdate_hash: "f1747cf60058e99f1bb9099ee9787d16c10241313b7acec81810ea1b1e568c11 AppImageUpdate-x86_64.AppImage"
- os: windows-2022
name: "Windows-MinGW-w64"
@ -80,9 +80,9 @@ jobs:
architecture: "x64"
vcvars_arch: "amd64"
qt_ver: 6
qt_host: windows
qt_arch: ""
qt_version: "6.7.3"
qt_host: "windows"
qt_arch: "win64_msvc2022_64"
qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
nscurl_tag: "v24.9.26.122"
nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
@ -93,9 +93,9 @@ jobs:
architecture: "arm64"
vcvars_arch: "amd64_arm64"
qt_ver: 6
qt_host: windows
qt_arch: "win64_msvc2019_arm64"
qt_version: "6.7.3"
qt_host: "windows"
qt_arch: "win64_msvc2022_arm64_cross_compiled"
qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
nscurl_tag: "v24.9.26.122"
nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
@ -106,7 +106,7 @@ jobs:
qt_ver: 6
qt_host: mac
qt_arch: ""
qt_version: "6.7.3"
qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: macos-14
@ -216,14 +216,14 @@ jobs:
- name: Install host Qt (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
uses: jurplel/install-qt-action@v3
uses: jurplel/install-qt-action@v4
with:
aqtversion: "==3.1.*"
py7zrversion: ">=0.20.2"
version: ${{ matrix.qt_version }}
host: "windows"
target: "desktop"
arch: ""
arch: ${{ matrix.qt_arch }}
modules: ${{ matrix.qt_modules }}
cache: ${{ inputs.is_qt_cached }}
cache-key-prefix: host-qt-arm64-windows
@ -232,7 +232,7 @@ jobs:
- name: Install Qt (macOS, Linux & Windows MSVC)
if: matrix.msystem == ''
uses: jurplel/install-qt-action@v3
uses: jurplel/install-qt-action@v4
with:
aqtversion: "==3.1.*"
py7zrversion: ">=0.20.2"
@ -252,19 +252,26 @@ jobs:
- name: Prepare AppImage (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5
env:
APPIMAGEUPDATE_HASH: ${{ matrix.appimageupdate_hash }}
LINUXDEPLOY_HASH: ${{ matrix.linuxdeploy_hash }}
LINUXDEPLOY_QT_HASH: ${{ matrix.linuxdeploy_qt_hash }}
run: |
wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
wget "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage"
wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage"
wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage"
wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-x86_64.AppImage"
wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage"
wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-x86_64.AppImage"
sha256sum -c - <<< "$LINUXDEPLOY_HASH"
sha256sum -c - <<< "$LINUXDEPLOY_QT_HASH"
sha256sum -c - <<< "$APPIMAGEUPDATE_HASH"
sudo apt install libopengl0 libfuse2
- name: Add QT_HOST_PATH var (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
run: |
echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2019_64" >> $env:GITHUB_ENV
echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2022_64" >> $env:GITHUB_ENV
- name: Setup java (macOS)
if: runner.os == 'macOS'
@ -629,76 +636,3 @@ jobs:
shell: msys2 {0}
run: |
ccache -s
flatpak:
runs-on: ubuntu-latest
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.8
options: --privileged
steps:
- name: Checkout
uses: actions/checkout@v4
if: inputs.build_type == 'Debug'
with:
submodules: true
- name: Set short version
shell: bash
run: echo "VERSION=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Build Flatpak (Linux)
if: inputs.build_type == 'Debug'
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: ShatteredPrism-${{ runner.os }}-${{ env.VERSION }}-Flatpak.flatpak
manifest-path: flatpak/org.lunaislazier.ShatteredPrism.yml
nix:
name: Nix (${{ matrix.system }})
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
system: x86_64-linux
- os: macos-13
system: x86_64-darwin
- os: macos-14
system: aarch64-darwin
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@v30
# For PRs
- name: Setup Nix Magic Cache
uses: DeterminateSystems/magic-nix-cache-action@v8
# For in-tree builds
- name: Setup Cachix
uses: cachix/cachix-action@v15
with:
name: unmojang
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Run flake checks
run: |
nix flake check --print-build-logs --show-trace
- name: Build debug package
if: ${{ inputs.build_type == 'Debug' }}
run: |
nix build --print-build-logs .#shatteredprism-debug
- name: Build release package
if: ${{ inputs.build_type != 'Debug' }}
run: |
nix build --print-build-logs .#shatteredprism

62
.github/workflows/flatpak.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: Flatpak
on:
push:
paths-ignore:
- "**.md"
- "**/LICENSE"
- ".github/ISSUE_TEMPLATE/**"
- ".markdownlint**"
- "nix/**"
# We don't do anything with these artifacts on releases. They go to Flathub
tags-ignore:
- "*"
pull_request:
paths-ignore:
- "**.md"
- "**/LICENSE"
- ".github/ISSUE_TEMPLATE/**"
- ".markdownlint**"
- "nix/**"
workflow_dispatch:
permissions:
contents: read
jobs:
build:
name: Build (${{ matrix.arch }})
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
arch: x86_64
- os: ubuntu-22.04-arm
arch: aarch64
runs-on: ${{ matrix.os }}
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.8
options: --privileged
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: true
- name: Set short version
shell: bash
run: |
echo "VERSION=${GITHUB_SHA::7}" >> "$GITHUB_ENV"
- name: Build Flatpak
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: FjordLauncher-${{ runner.os }}-${{ env.VERSION }}-Flatpak.flatpak
manifest-path: flatpak/org.fjordlauncher.FjordLauncher.yml
arch: ${{ matrix.arch }}

88
.github/workflows/nix.yml vendored Normal file
View File

@ -0,0 +1,88 @@
name: Nix
on:
push:
paths-ignore:
- "**.md"
- "**/LICENSE"
- ".github/ISSUE_TEMPLATE/**"
- ".markdownlint**"
- "flatpak/**"
tags:
- "*"
pull_request_target:
paths-ignore:
- "**.md"
- "**/LICENSE"
- ".github/ISSUE_TEMPLATE/**"
- ".markdownlint**"
- "flatpak/**"
workflow_dispatch:
permissions:
contents: read
env:
DEBUG: ${{ github.ref_type != 'tag' }}
USE_DETERMINATE: ${{ github.event_name == 'pull_request' }}
jobs:
build:
name: Build (${{ matrix.system }})
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
system: x86_64-linux
- os: ubuntu-22.04-arm
system: aarch64-linux
- os: macos-13
system: x86_64-darwin
- os: macos-14
system: aarch64-darwin
runs-on: ${{ matrix.os }}
permissions:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v16
with:
determinate: ${{ env.USE_DETERMINATE }}
# For PRs
- name: Setup Nix Magic Cache
if: ${{ env.USE_DETERMINATE }}
uses: DeterminateSystems/flakehub-cache-action@v1
# For in-tree builds
- name: Setup Cachix
if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
uses: cachix/cachix-action@v15
with:
name: fjordlauncher
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Run Flake checks
run: |
nix flake check --print-build-logs --show-trace
- name: Build debug package
if: ${{ env.DEBUG }}
run: |
nix build --print-build-logs .#fjordlauncher-debug
- name: Build release package
if: ${{ !env.DEBUG }}
run: |
nix build --print-build-logs .#fjordlauncher

45
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: Publish
on:
release:
types: [ released ]
permissions:
contents: read
jobs:
flakehub:
name: FlakeHub
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- name: Install Nix
uses: cachix/install-nix-action@v30
- name: Publish on FlakeHub
uses: determinatesystems/flakehub-push@v5
with:
visibility: "public"
winget:
name: Winget
runs-on: windows-latest
steps:
- name: Publish on Winget
uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: PrismLauncher.PrismLauncher
version: ${{ github.event.release.tag_name }}
installers-regex: 'PrismLauncher-Windows-MSVC(:?-arm64|-Legacy)?-Setup-.+\.exe$'
token: ${{ secrets.WINGET_TOKEN }}

View File

@ -38,6 +38,5 @@ jobs:
APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}

View File

@ -23,7 +23,6 @@ jobs:
APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}

View File

@ -1,15 +0,0 @@
name: Publish to WinGet
on:
release:
types: [released]
jobs:
publish:
runs-on: windows-latest
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: PrismLauncher.PrismLauncher
version: ${{ github.event.release.tag_name }}
installers-regex: 'PrismLauncher-Windows-MSVC(:?-arm64|-Legacy)?-Setup-.+\.exe$'
token: ${{ secrets.WINGET_TOKEN }}

6
.gitignore vendored
View File

@ -47,8 +47,12 @@ run/
# Nix/NixOS
.direnv/
.pre-commit-config.yaml
## Used when manually invoking stdenv phases
outputs/
## Regular artifacts
result
result-*
repl-result-*
# Flatpak
.flatpak-builder

View File

@ -78,6 +78,13 @@ else()
# ATL's pack list needs more than the default 1 Mib stack on windows
if(WIN32)
set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}")
# -ffunction-sections and -fdata-sections help reduce binary size
# -mguard=cf enables Control Flow Guard
# TODO: Look into -gc-sections to further reduce binary size
foreach(lang C CXX)
set("CMAKE_${lang}_FLAGS_RELEASE" "-ffunction-sections -fdata-sections -mguard=cf")
endforeach()
endif()
endif()
@ -106,14 +113,14 @@ if ((CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebI
else()
# AppleClang and Clang
message(STATUS "Address Sanitizer available on Clang")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=null")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=null")
endif()
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
# GCC
message(STATUS "Address Sanitizer available on GCC")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover")
link_libraries("asan")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
message(STATUS "Address Sanitizer available on MSVC")
@ -181,7 +188,7 @@ set(Launcher_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE S
######## Set version numbers ########
set(Launcher_VERSION_MAJOR 1)
set(Launcher_VERSION_MINOR 6)
set(Launcher_VERSION_MINOR 7)
set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")
set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.0.0")

View File

@ -1,9 +1,4 @@
(import (
let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
in
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash;
}
) { src = ./.; }).defaultNix
(import (fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/ff81ac966bb2cae68946d5ed5fc4994f96d0ffec.tar.gz";
sha256 = "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=";
}) { src = ./.; }).defaultNix

131
flake.nix
View File

@ -15,28 +15,6 @@
url = "github:PrismLauncher/libnbtplusplus";
flake = false;
};
nix-filter.url = "github:numtide/nix-filter";
/*
Inputs below this are optional and can be removed
```
{
inputs.shatteredprism = {
url = "github:lunaislazier/ShatteredPrism";
inputs = {
flake-compat.follows = "";
};
};
}
```
*/
flake-compat = {
url = "github:edolstra/flake-compat";
flake = false;
};
};
outputs =
@ -44,9 +22,8 @@
self,
nixpkgs,
libnbtplusplus,
nix-filter,
...
}:
let
inherit (nixpkgs) lib;
@ -58,27 +35,108 @@
forAllSystems = lib.genAttrs systems;
nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system});
in
{
checks = forAllSystems (
system:
let
checks' = nixpkgsFor.${system}.callPackage ./nix/checks.nix { inherit self; };
pkgs = nixpkgsFor.${system};
llvm = pkgs.llvmPackages_19;
in
lib.filterAttrs (_: lib.isDerivation) checks'
{
formatting =
pkgs.runCommand "check-formatting"
{
nativeBuildInputs = with pkgs; [
deadnix
llvm.clang-tools
markdownlint-cli
nixfmt-rfc-style
statix
];
}
''
cd ${self}
echo "Running clang-format...."
clang-format --dry-run --style='file' --Werror */**.{c,cc,cpp,h,hh,hpp}
echo "Running deadnix..."
deadnix --fail
echo "Running markdownlint..."
markdownlint --dot .
echo "Running nixfmt..."
find -type f -name '*.nix' -exec nixfmt --check {} +
echo "Running statix"
statix check .
touch $out
'';
}
);
devShells = forAllSystems (
system:
let
pkgs = nixpkgsFor.${system};
llvm = pkgs.llvmPackages_19;
packages' = self.packages.${system};
# Re-use our package wrapper to wrap our development environment
qt-wrapper-env = packages'.shatteredprism.overrideAttrs (old: {
name = "qt-wrapper-env";
# Required to use script-based makeWrapper below
strictDeps = true;
# We don't need/want the unwrapped Fjord package
paths = [ ];
nativeBuildInputs = old.nativeBuildInputs or [ ] ++ [
# Ensure the wrapper is script based so it can be sourced
pkgs.makeWrapper
];
# Inspired by https://discourse.nixos.org/t/python-qt-woes/11808/10
buildCommand = ''
makeQtWrapper ${lib.getExe pkgs.runtimeShellPackage} "$out"
sed -i '/^exec/d' "$out"
'';
});
in
{
default = pkgs.mkShell {
inputsFrom = [ self.packages.${system}.shatteredprism-unwrapped ];
buildInputs = with pkgs; [
inputsFrom = [ packages'.shatteredprism-unwrapped ];
packages = with pkgs; [
ccache
ninja
llvm.clang-tools
];
cmakeBuildType = "Debug";
cmakeFlags = [ "-GNinja" ] ++ packages'.shatteredprism.cmakeFlags;
dontFixCmake = true;
shellHook = ''
echo "Sourcing ${qt-wrapper-env}"
source ${qt-wrapper-env}
git submodule update --init --force
if [ ! -f compile_commands.json ]; then
cmakeConfigurePhase
cd ..
ln -s "$cmakeBuildDir"/compile_commands.json compile_commands.json
fi
'';
};
}
);
@ -89,7 +147,6 @@
shatteredprism-unwrapped = prev.callPackage ./nix/unwrapped.nix {
inherit
libnbtplusplus
nix-filter
self
;
};
@ -99,6 +156,7 @@
packages = forAllSystems (
system:
let
pkgs = nixpkgsFor.${system};
@ -111,6 +169,7 @@
default = shatteredPackages.shatteredprism;
};
in
# Only output them if they're available on the current system
lib.filterAttrs (_: lib.meta.availableOn pkgs.stdenv.hostPlatform) packages
);
@ -118,16 +177,18 @@
# We put these under legacyPackages as they are meant for CI, not end user consumption
legacyPackages = forAllSystems (
system:
let
shatteredPackages = self.packages.${system};
legacyPackages = self.legacyPackages.${system};
packages' = self.packages.${system};
legacyPackages' = self.legacyPackages.${system};
in
{
shatteredprism-debug = shatteredPackages.shatteredprism.override {
shatteredprism-unwrapped = legacyPackages.shatteredprism-unwrapped-debug;
shatteredprism-debug = packages'.shatteredprism.override {
shatteredprism-unwrapped = legacyPackages'.shatteredprism-unwrapped-debug;
};
shatteredprism-unwrapped-debug = shatteredPackages.shatteredprism-unwrapped.overrideAttrs {
shatteredprism-unwrapped-debug = packages'.shatteredprism-unwrapped.overrideAttrs {
cmakeBuildType = "Debug";
dontStrip = true;
};

View File

@ -160,6 +160,7 @@
#if defined Q_OS_WIN32
#include <windows.h>
#include <QStyleHints>
#include "WindowsConsole.h"
#endif
@ -231,7 +232,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
setApplicationDisplayName(QString("%1 %2").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()));
setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT);
setDesktopFileName(BuildConfig.LAUNCHER_DESKTOPFILENAME);
startTime = QDateTime::currentDateTime();
m_startTime = QDateTime::currentDateTime();
// Don't quit on hiding the last window
this->setQuitOnLastWindowClosed(false);
@ -1124,8 +1125,16 @@ bool Application::createSetupWizard()
// set default theme after going into theme wizard
if (!validIcons)
settings()->set("IconTheme", QString("pe_colored"));
if (!validWidgets)
settings()->set("ApplicationTheme", QString("system"));
if (!validWidgets) {
#if defined(Q_OS_WIN32) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
const QString style =
QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark ? QStringLiteral("dark") : QStringLiteral("bright");
#else
const QString style = QStringLiteral("system");
#endif
settings()->set("ApplicationTheme", style);
}
m_themeManager->applyCurrentlySelectedTheme(true);
@ -1192,6 +1201,9 @@ bool Application::event(QEvent* event)
#endif
if (event->type() == QEvent::FileOpen) {
if (!m_mainWindow) {
showMainWindow(false);
}
auto ev = static_cast<QFileOpenEvent*>(event);
m_mainWindow->processURLs({ ev->url() });
}
@ -1350,6 +1362,9 @@ void Application::messageReceived(const QByteArray& message)
qWarning() << "Received" << command << "message without a zip path/URL.";
return;
}
if (!m_mainWindow) {
showMainWindow(false);
}
m_mainWindow->processURLs({ normalizeImportUrl(url) });
} else if (command == "launch") {
QString id = received.args["id"];

View File

@ -112,7 +112,7 @@ class Application : public QApplication {
std::shared_ptr<SettingsObject> settings() const { return m_settings; }
qint64 timeSinceStart() const { return startTime.msecsTo(QDateTime::currentDateTime()); }
qint64 timeSinceStart() const { return m_startTime.msecsTo(QDateTime::currentDateTime()); }
QIcon getThemedIcon(const QString& name);
@ -236,7 +236,7 @@ class Application : public QApplication {
bool shouldExitNow() const;
private:
QDateTime startTime;
QDateTime m_startTime;
shared_qobject_ptr<QNetworkAccessManager> m_network;

View File

@ -1066,8 +1066,6 @@ SET(LAUNCHER_SOURCES
ui/dialogs/CopyInstanceDialog.h
ui/dialogs/CustomMessageBox.cpp
ui/dialogs/CustomMessageBox.h
ui/dialogs/EditAccountDialog.cpp
ui/dialogs/EditAccountDialog.h
ui/dialogs/ExportInstanceDialog.cpp
ui/dialogs/ExportInstanceDialog.h
ui/dialogs/ExportPackDialog.cpp
@ -1257,7 +1255,6 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/OfflineLoginDialog.ui
ui/dialogs/AuthlibInjectorLoginDialog.ui
ui/dialogs/AboutDialog.ui
ui/dialogs/EditAccountDialog.ui
ui/dialogs/ReviewMessageBox.ui
ui/dialogs/ScrollMessageBox.ui
ui/dialogs/BlockedModsDialog.ui

View File

@ -307,6 +307,7 @@ void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw)
if (!replacing) {
roles.clear();
filterModel->setSourceModel(replacing);
endResetModel();
return;
}

View File

@ -171,7 +171,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status)
auto os_arch = results["os.arch"];
auto java_version = results["java.version"];
auto java_vendor = results["java.vendor"];
bool is_64 = os_arch == "x86_64" || os_arch == "amd64" || os_arch == "aarch64" || os_arch == "arm64";
bool is_64 = os_arch == "x86_64" || os_arch == "amd64" || os_arch == "aarch64" || os_arch == "arm64" || os_arch == "riscv64";
result.validity = Result::Validity::Valid;
result.is_64bit = is_64;

View File

@ -254,20 +254,60 @@ void LaunchTask::emitFailed(QString reason)
Task::emitFailed(reason);
}
void LaunchTask::substituteVariables(QStringList& args) const
QString expandVariables(const QString& input, QProcessEnvironment dict)
{
auto env = m_instance->createEnvironment();
QString result = input;
for (auto key : env.keys()) {
args.replaceInStrings("$" + key, env.value(key));
enum { base, maybeBrace, variable, brace } state = base;
int startIdx = -1;
for (int i = 0; i < result.length();) {
QChar c = result.at(i++);
switch (state) {
case base:
if (c == '$')
state = maybeBrace;
break;
case maybeBrace:
if (c == '{') {
state = brace;
startIdx = i;
} else if (c.isLetterOrNumber() || c == '_') {
state = variable;
startIdx = i - 1;
} else {
state = base;
}
break;
case brace:
if (c == '}') {
const auto res = dict.value(result.mid(startIdx, i - 1 - startIdx), "");
if (!res.isEmpty()) {
result.replace(startIdx - 2, i - startIdx + 2, res);
i = startIdx - 2 + res.length();
}
state = base;
}
break;
case variable:
if (!c.isLetterOrNumber() && c != '_') {
const auto res = dict.value(result.mid(startIdx, i - startIdx - 1), "");
if (!res.isEmpty()) {
result.replace(startIdx - 1, i - startIdx, res);
i = startIdx - 1 + res.length();
}
state = base;
}
break;
}
}
if (state == variable) {
if (const auto res = dict.value(result.mid(startIdx), ""); !res.isEmpty())
result.replace(startIdx - 1, result.length() - startIdx + 1, res);
}
return result;
}
void LaunchTask::substituteVariables(QString& cmd) const
QString LaunchTask::substituteVariables(QString& cmd, bool isLaunch) const
{
auto env = m_instance->createEnvironment();
for (auto key : env.keys()) {
cmd.replace("$" + key, env.value(key));
}
return expandVariables(cmd, isLaunch ? m_instance->createLaunchEnvironment() : m_instance->createEnvironment());
}

View File

@ -87,8 +87,7 @@ class LaunchTask : public Task {
shared_qobject_ptr<LogModel> getLogModel();
public:
void substituteVariables(QStringList& args) const;
void substituteVariables(QString& cmd) const;
QString substituteVariables(QString& cmd, bool isLaunch = false) const;
QString censorPrivateInfo(QString in);
protected: /* methods */

View File

@ -32,7 +32,7 @@ class LogModel : public QAbstractListModel {
private /* types */:
struct entry {
MessageLevel::Enum level;
MessageLevel::Enum level = MessageLevel::Enum::Unknown;
QString line;
};

View File

@ -47,19 +47,15 @@ PostLaunchCommand::PostLaunchCommand(LaunchTask* parent) : LaunchStep(parent)
void PostLaunchCommand::executeTask()
{
// FIXME: where to put this?
auto cmd = m_parent->substituteVariables(m_command);
emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
auto args = QProcess::splitCommand(m_command);
m_parent->substituteVariables(args);
auto args = QProcess::splitCommand(cmd);
emit logLine(tr("Running Post-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher);
const QString program = args.takeFirst();
m_process.start(program, args);
#else
m_parent->substituteVariables(m_command);
emit logLine(tr("Running Post-Launch command: %1").arg(m_command), MessageLevel::Launcher);
m_process.start(m_command);
m_process.start(cmd);
#endif
}

View File

@ -47,19 +47,14 @@ PreLaunchCommand::PreLaunchCommand(LaunchTask* parent) : LaunchStep(parent)
void PreLaunchCommand::executeTask()
{
// FIXME: where to put this?
auto cmd = m_parent->substituteVariables(m_command);
emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), MessageLevel::Launcher);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
auto args = QProcess::splitCommand(m_command);
m_parent->substituteVariables(args);
emit logLine(tr("Running Pre-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher);
auto args = QProcess::splitCommand(cmd);
const QString program = args.takeFirst();
m_process.start(program, args);
#else
m_parent->substituteVariables(m_command);
emit logLine(tr("Running Pre-Launch command: %1").arg(m_command), MessageLevel::Launcher);
m_process.start(m_command);
m_process.start(cmd);
#endif
}

View File

@ -654,6 +654,7 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
// dlsym variant is only needed for OpenGL and not included in the vulkan layer
appendLib("libMangoHud_dlsym.so");
appendLib("libMangoHud_opengl.so");
appendLib("libMangoHud_shim.so");
preloadList << mangoHudLibString;
}
@ -1131,13 +1132,6 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(step);
}
// run pre-launch command if that's needed
if (getPreLaunchCommand().size()) {
auto step = makeShared<PreLaunchCommand>(pptr);
step->setWorkingDirectory(gameRoot());
process->appendStep(step);
}
// load meta
{
auto mode = session->status != AuthSession::PlayableOffline ? Net::Mode::Online : Net::Mode::Offline;
@ -1150,6 +1144,13 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(makeShared<CheckJava>(pptr));
}
// run pre-launch command if that's needed
if (getPreLaunchCommand().size()) {
auto step = makeShared<PreLaunchCommand>(pptr);
step->setWorkingDirectory(gameRoot());
process->appendStep(step);
}
// if we aren't in offline mode,.
if (session->status != AuthSession::PlayableOffline) {
if (!session->demo) {

View File

@ -8,7 +8,10 @@ void MinecraftLoadAndCheck::executeTask()
{
// add offline metadata load task
auto components = m_inst->getPackProfile();
components->reload(m_netmode);
if (auto result = components->reload(m_netmode); !result) {
emitFailed(result.error);
return;
}
m_task = components->getCurrentTask();
if (!m_task) {

View File

@ -180,29 +180,32 @@ static bool savePackProfile(const QString& filename, const ComponentContainer& c
}
// Read the given file into component containers
static bool loadPackProfile(PackProfile* parent,
const QString& filename,
const QString& componentJsonPattern,
ComponentContainer& container)
static PackProfile::Result loadPackProfile(PackProfile* parent,
const QString& filename,
const QString& componentJsonPattern,
ComponentContainer& container)
{
QFile componentsFile(filename);
if (!componentsFile.exists()) {
qCWarning(instanceProfileC) << "Components file" << filename << "doesn't exist. This should never happen.";
return false;
auto message = QObject::tr("Components file %1 doesn't exist. This should never happen.").arg(filename);
qCWarning(instanceProfileC) << message;
return PackProfile::Result::Error(message);
}
if (!componentsFile.open(QFile::ReadOnly)) {
qCCritical(instanceProfileC) << "Couldn't open" << componentsFile.fileName() << " for reading:" << componentsFile.errorString();
auto message = QObject::tr("Couldn't open %1 for reading: %2").arg(componentsFile.fileName(), componentsFile.errorString());
qCCritical(instanceProfileC) << message;
qCWarning(instanceProfileC) << "Ignoring overridden order";
return false;
return PackProfile::Result::Error(message);
}
// and it's valid JSON
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString();
auto message = QObject::tr("Couldn't parse %1 as json: %2").arg(componentsFile.fileName(), error.errorString());
qCCritical(instanceProfileC) << message;
qCWarning(instanceProfileC) << "Ignoring overridden order";
return false;
return PackProfile::Result::Error(message);
}
// and then read it and process it if all above is true.
@ -219,11 +222,13 @@ static bool loadPackProfile(PackProfile* parent,
container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj));
}
} catch ([[maybe_unused]] const JSONValidationError& err) {
qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ": bad file format";
auto message = QObject::tr("Couldn't parse %1 : bad file format").arg(componentsFile.fileName());
qCCritical(instanceProfileC) << message;
qCWarning(instanceProfileC) << "error:" << err.what();
container.clear();
return false;
return PackProfile::Result::Error(message);
}
return true;
return PackProfile::Result::Success();
}
// END: component file format
@ -290,44 +295,43 @@ void PackProfile::save_internal()
d->dirty = false;
}
bool PackProfile::load()
PackProfile::Result PackProfile::load()
{
auto filename = componentsFilePath();
// load the new component list and swap it with the current one...
ComponentContainer newComponents;
if (!loadPackProfile(this, filename, patchesPattern(), newComponents)) {
if (auto result = loadPackProfile(this, filename, patchesPattern(), newComponents); !result) {
qCritical() << d->m_instance->name() << "|" << "Failed to load the component config";
return false;
} else {
// FIXME: actually use fine-grained updates, not this...
beginResetModel();
// disconnect all the old components
for (auto component : d->components) {
disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
}
d->components.clear();
d->componentIndex.clear();
for (auto component : newComponents) {
if (d->componentIndex.contains(component->m_uid)) {
qWarning() << d->m_instance->name() << "|" << "Ignoring duplicate component entry" << component->m_uid;
continue;
}
connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
d->components.append(component);
d->componentIndex[component->m_uid] = component;
}
endResetModel();
d->loaded = true;
return true;
return result;
}
// FIXME: actually use fine-grained updates, not this...
beginResetModel();
// disconnect all the old components
for (auto component : d->components) {
disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
}
d->components.clear();
d->componentIndex.clear();
for (auto component : newComponents) {
if (d->componentIndex.contains(component->m_uid)) {
qWarning() << d->m_instance->name() << "|" << "Ignoring duplicate component entry" << component->m_uid;
continue;
}
connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
d->components.append(component);
d->componentIndex[component->m_uid] = component;
}
endResetModel();
d->loaded = true;
return Result::Success();
}
void PackProfile::reload(Net::Mode netmode)
PackProfile::Result PackProfile::reload(Net::Mode netmode)
{
// Do not reload when the update/resolve task is running. It is in control.
if (d->m_updateTask) {
return;
return Result::Success();
}
// flush any scheduled saves to not lose state
@ -336,9 +340,11 @@ void PackProfile::reload(Net::Mode netmode)
// FIXME: differentiate when a reapply is required by propagating state from components
invalidateLaunchProfile();
if (load()) {
resolve(netmode);
if (auto result = load(); !result) {
return result;
}
resolve(netmode);
return Result::Success();
}
Task::Ptr PackProfile::getCurrentTask()

View File

@ -62,6 +62,19 @@ class PackProfile : public QAbstractListModel {
public:
enum Columns { NameColumn = 0, VersionColumn, NUM_COLUMNS };
struct Result {
bool success;
QString error;
// Implicit conversion to bool
operator bool() const { return success; }
// Factory methods for convenience
static Result Success() { return { true, "" }; }
static Result Error(const QString& errorMessage) { return { false, errorMessage }; }
};
explicit PackProfile(MinecraftInstance* instance);
virtual ~PackProfile();
@ -102,7 +115,7 @@ class PackProfile : public QAbstractListModel {
bool revertToBase(int index);
/// reload the list, reload all components, resolve dependencies
void reload(Net::Mode netmode);
Result reload(Net::Mode netmode);
// reload all components, resolve dependencies
void resolve(Net::Mode netmode);
@ -169,7 +182,7 @@ class PackProfile : public QAbstractListModel {
void disableInteraction(bool disable);
private:
bool load();
Result load();
bool installJarMods_internal(QStringList filepaths);
bool installCustomJar_internal(QString filepath);
bool installAgents_internal(QStringList filepaths);

View File

@ -99,44 +99,44 @@ MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(sile
</script>
)XXX")
.arg(BuildConfig.LOGIN_CALLBACK_URL));
oauth2.setReplyHandler(replyHandler);
m_oauth2.setReplyHandler(replyHandler);
} else {
oauth2.setReplyHandler(new CustomOAuthOobReplyHandler(this));
m_oauth2.setReplyHandler(new CustomOAuthOobReplyHandler(this));
}
oauth2.setAuthorizationUrl(QUrl("https://login.live.com/oauth20_connect.srf"));
oauth2.setAccessTokenUrl(QUrl("https://login.live.com/oauth20_token.srf"));
m_oauth2.setAuthorizationUrl(QUrl("https://login.live.com/oauth20_connect.srf"));
m_oauth2.setAccessTokenUrl(QUrl("https://login.live.com/oauth20_token.srf"));
const auto& scope = "service::user.auth.xboxlive.com::MBI_SSL";
oauth2.setScope(scope);
m_oauth2.setScope(scope);
// QOAuth2AuthorizationCodeFlow doesn't pass a "scope" when refreshing access tokens, but Microsoft expects it.
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
oauth2.setModifyParametersFunction([&](QAbstractOAuth::Stage stage, QVariantMap* parameters) {
m_oauth2.setModifyParametersFunction([&](QAbstractOAuth::Stage stage, QVariantMap* parameters) {
if (stage == QAbstractOAuth::Stage::RefreshingAccessToken) {
(*parameters)["scope"] = scope;
}
});
#else
oauth2.setModifyParametersFunction([&](QAbstractOAuth::Stage stage, QMultiMap<QString, QVariant>* parameters) {
m_oauth2.setModifyParametersFunction([&](QAbstractOAuth::Stage stage, QMultiMap<QString, QVariant>* parameters) {
if (stage == QAbstractOAuth::Stage::RefreshingAccessToken) {
(*parameters).insert("scope", scope);
}
});
#endif
oauth2.setClientIdentifier(m_clientId);
oauth2.setNetworkAccessManager(APPLICATION->network().get());
m_oauth2.setClientIdentifier(m_clientId);
m_oauth2.setNetworkAccessManager(APPLICATION->network().get());
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, [this] {
m_data->msaClientID = oauth2.clientIdentifier();
connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, [this] {
m_data->msaClientID = m_oauth2.clientIdentifier();
m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc();
m_data->msaToken.notAfter = oauth2.expirationAt();
m_data->msaToken.extra = oauth2.extraTokens();
m_data->msaToken.refresh_token = oauth2.refreshToken();
m_data->msaToken.token = oauth2.token();
m_data->msaToken.notAfter = m_oauth2.expirationAt();
m_data->msaToken.extra = m_oauth2.extraTokens();
m_data->msaToken.refresh_token = m_oauth2.refreshToken();
m_data->msaToken.token = m_oauth2.token();
emit finished(AccountTaskState::STATE_WORKING, tr("Got "));
});
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &MSAStep::authorizeWithBrowser);
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this, [this, silent](const QAbstractOAuth2::Error err) {
connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &MSAStep::authorizeWithBrowser);
connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this, [this, silent](const QAbstractOAuth2::Error err) {
auto state = AccountTaskState::STATE_FAILED_HARD;
if (oauth2.status() == QAbstractOAuth::Status::Granted || silent) {
if (m_oauth2.status() == QAbstractOAuth::Status::Granted || silent) {
if (err == QAbstractOAuth2::Error::NetworkError) {
state = AccountTaskState::STATE_OFFLINE;
} else {
@ -150,16 +150,16 @@ MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(sile
qWarning() << message;
emit finished(state, message);
});
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::error, this,
connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::error, this,
[this](const QString& error, const QString& errorDescription, const QUrl& uri) {
qWarning() << "Failed to login because" << error << errorDescription;
emit finished(AccountTaskState::STATE_FAILED_HARD, errorDescription);
});
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::extraTokensChanged, this,
connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::extraTokensChanged, this,
[this](const QVariantMap& tokens) { m_data->msaToken.extra = tokens; });
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::clientIdentifierChanged, this,
connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::clientIdentifierChanged, this,
[this](const QString& clientIdentifier) { m_data->msaClientID = clientIdentifier; });
}
@ -180,20 +180,20 @@ void MSAStep::perform()
emit finished(AccountTaskState::STATE_DISABLED, tr("Microsoft user authentication failed - refresh token is empty."));
return;
}
oauth2.setRefreshToken(m_data->msaToken.refresh_token);
oauth2.refreshAccessToken();
m_oauth2.setRefreshToken(m_data->msaToken.refresh_token);
m_oauth2.refreshAccessToken();
} else {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) // QMultiMap param changed in 6.0
oauth2.setModifyParametersFunction(
m_oauth2.setModifyParametersFunction(
[](QAbstractOAuth::Stage stage, QMultiMap<QString, QVariant>* map) { map->insert("prompt", "select_account"); });
#else
oauth2.setModifyParametersFunction(
m_oauth2.setModifyParametersFunction(
[](QAbstractOAuth::Stage stage, QMap<QString, QVariant>* map) { map->insert("prompt", "select_account"); });
#endif
*m_data = AccountData();
m_data->msaClientID = m_clientId;
oauth2.grant();
m_oauth2.grant();
}
}

View File

@ -55,5 +55,5 @@ class MSAStep : public AuthStep {
private:
bool m_silent;
QString m_clientId;
QOAuth2AuthorizationCodeFlow oauth2;
QOAuth2AuthorizationCodeFlow m_oauth2;
};

View File

@ -132,6 +132,7 @@ void LauncherPartLaunch::executeTask()
QString wrapperCommandStr = instance->getWrapperCommand().trimmed();
if (!wrapperCommandStr.isEmpty()) {
wrapperCommandStr = m_parent->substituteVariables(wrapperCommandStr);
auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr);
auto wrapperCommand = wrapperArgs.takeFirst();
auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand);
@ -171,6 +172,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state)
case LoggedProcess::Aborted:
case LoggedProcess::Crashed: {
m_parent->setPid(-1);
m_parent->instance()->setMinecraftRunning(false);
emitFailed(tr("Game crashed."));
return;
}

View File

@ -293,86 +293,90 @@ ModDetails ReadFabricModInfo(QByteArray contents)
// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md
ModDetails ReadQuiltModInfo(QByteArray contents)
{
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = Json::requireObject(jsonDoc, "quilt.mod.json");
auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version");
ModDetails details;
try {
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = Json::requireObject(jsonDoc, "quilt.mod.json");
auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version");
// https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md
if (schemaVersion == 1) {
auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info");
// https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md
if (schemaVersion == 1) {
auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info");
details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
details.version = Json::requireString(modInfo.value("version"), "Mod version");
details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
details.version = Json::requireString(modInfo.value("version"), "Mod version");
auto modMetadata = Json::ensureObject(modInfo.value("metadata"));
auto modMetadata = Json::ensureObject(modInfo.value("metadata"));
details.name = Json::ensureString(modMetadata.value("name"), details.mod_id);
details.description = Json::ensureString(modMetadata.value("description"));
details.name = Json::ensureString(modMetadata.value("name"), details.mod_id);
details.description = Json::ensureString(modMetadata.value("description"));
auto modContributors = Json::ensureObject(modMetadata.value("contributors"));
auto modContributors = Json::ensureObject(modMetadata.value("contributors"));
// We don't really care about the role of a contributor here
details.authors += modContributors.keys();
// We don't really care about the role of a contributor here
details.authors += modContributors.keys();
auto modContact = Json::ensureObject(modMetadata.value("contact"));
auto modContact = Json::ensureObject(modMetadata.value("contact"));
if (modContact.contains("homepage")) {
details.homeurl = Json::requireString(modContact.value("homepage"));
}
if (modContact.contains("issues")) {
details.issue_tracker = Json::requireString(modContact.value("issues"));
}
if (modContact.contains("homepage")) {
details.homeurl = Json::requireString(modContact.value("homepage"));
}
if (modContact.contains("issues")) {
details.issue_tracker = Json::requireString(modContact.value("issues"));
}
if (modMetadata.contains("license")) {
auto license = modMetadata.value("license");
if (license.isArray()) {
for (auto l : license.toArray()) {
if (l.isString()) {
details.licenses.append(ModLicense(l.toString()));
} else if (l.isObject()) {
auto obj = l.toObject();
details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(),
obj.value("url").toString(), obj.value("description").toString()));
if (modMetadata.contains("license")) {
auto license = modMetadata.value("license");
if (license.isArray()) {
for (auto l : license.toArray()) {
if (l.isString()) {
details.licenses.append(ModLicense(l.toString()));
} else if (l.isObject()) {
auto obj = l.toObject();
details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(),
obj.value("url").toString(), obj.value("description").toString()));
}
}
} else if (license.isString()) {
details.licenses.append(ModLicense(license.toString()));
} else if (license.isObject()) {
auto obj = license.toObject();
details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(),
obj.value("url").toString(), obj.value("description").toString()));
}
}
if (modMetadata.contains("icon")) {
auto icon = modMetadata.value("icon");
if (icon.isObject()) {
auto obj = icon.toObject();
// take the largest icon
int largest = 0;
for (auto key : obj.keys()) {
auto size = key.split('x').first().toInt();
if (size > largest) {
largest = size;
}
}
if (largest > 0) {
auto key = QString::number(largest) + "x" + QString::number(largest);
details.icon_file = obj.value(key).toString();
} else { // parsing the sizes failed
// take the first
for (auto i : obj) {
details.icon_file = i.toString();
break;
}
}
} else if (icon.isString()) {
details.icon_file = icon.toString();
}
} else if (license.isString()) {
details.licenses.append(ModLicense(license.toString()));
} else if (license.isObject()) {
auto obj = license.toObject();
details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), obj.value("url").toString(),
obj.value("description").toString()));
}
}
if (modMetadata.contains("icon")) {
auto icon = modMetadata.value("icon");
if (icon.isObject()) {
auto obj = icon.toObject();
// take the largest icon
int largest = 0;
for (auto key : obj.keys()) {
auto size = key.split('x').first().toInt();
if (size > largest) {
largest = size;
}
}
if (largest > 0) {
auto key = QString::number(largest) + "x" + QString::number(largest);
details.icon_file = obj.value(key).toString();
} else { // parsing the sizes failed
// take the first
for (auto i : obj) {
details.icon_file = i.toString();
break;
}
}
} else if (icon.isString()) {
details.icon_file = icon.toString();
}
}
} catch (const Exception& e) {
qWarning() << "Unable to parse mod info:" << e.cause();
}
return details;
}

View File

@ -73,7 +73,7 @@ class ResourceAPI {
std::optional<QString> search;
std::optional<SortingMethod> sorting;
std::optional<ModPlatform::ModLoaderTypes> loaders;
std::optional<std::list<Version> > versions;
std::optional<std::list<Version>> versions;
std::optional<QString> side;
std::optional<QStringList> categoryIds;
};
@ -168,11 +168,23 @@ class ResourceAPI {
protected:
[[nodiscard]] inline QString debugName() const { return "External resource API"; }
[[nodiscard]] inline auto getGameVersionsString(std::list<Version> mcVersions) const -> QString
[[nodiscard]] inline QString mapMCVersionToModrinth(Version v) const
{
static const QString preString = " Pre-Release ";
auto verStr = v.toString();
if (verStr.contains(preString)) {
verStr.replace(preString, "-pre");
}
verStr.replace(" ", "-");
return verStr;
}
[[nodiscard]] inline QString getGameVersionsString(std::list<Version> mcVersions) const
{
QString s;
for (auto& ver : mcVersions) {
s += QString("\"%1\",").arg(ver.toString());
s += QString("\"%1\",").arg(mapMCVersionToModrinth(ver));
}
s.remove(s.length() - 1, 1); // remove last comma
return s;

View File

@ -112,6 +112,8 @@ void Flame::FileResolvingTask::netJobFinished()
auto obj = Json::requireObject(file);
auto version = FlameMod::loadIndexedPackVersion(obj);
auto fileid = version.fileId.toInt();
Q_ASSERT(fileid != 0);
Q_ASSERT(m_manifest.files.contains(fileid));
m_manifest.files[fileid].version = version;
auto url = QUrl(version.downloadUrl, QUrl::TolerantMode);
if (!url.isValid() && "sha1" == version.hash_type && !version.hash.isEmpty()) {

View File

@ -270,6 +270,7 @@ std::optional<ModPlatform::IndexedVersion> FlameAPI::getLatestVersion(QList<ModP
QList<ModPlatform::ModLoaderType> instanceLoaders,
ModPlatform::ModLoaderTypes modLoaders)
{
static const auto noLoader = ModPlatform::ModLoaderType(0);
QHash<ModPlatform::ModLoaderType, ModPlatform::IndexedVersion> bestMatch;
auto checkVersion = [&bestMatch](const ModPlatform::IndexedVersion& version, const ModPlatform::ModLoaderType& loader) {
if (bestMatch.contains(loader)) {
@ -284,7 +285,7 @@ std::optional<ModPlatform::IndexedVersion> FlameAPI::getLatestVersion(QList<ModP
for (auto file_tmp : versions) {
auto loaders = ModPlatform::modLoaderTypesToList(file_tmp.loaders);
if (loaders.isEmpty()) {
checkVersion(file_tmp, ModPlatform::ModLoaderType(0));
checkVersion(file_tmp, noLoader);
} else {
for (auto loader : loaders) {
checkVersion(file_tmp, loader);
@ -293,11 +294,19 @@ std::optional<ModPlatform::IndexedVersion> FlameAPI::getLatestVersion(QList<ModP
}
// edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on update
auto currentLoaders = instanceLoaders + ModPlatform::modLoaderTypesToList(modLoaders);
currentLoaders.append(ModPlatform::ModLoaderType(0)); // add a fallback in case the versions do not define a loader
currentLoaders.append(noLoader); // add a fallback in case the versions do not define a loader
for (auto loader : currentLoaders) {
if (bestMatch.contains(loader)) {
return bestMatch.value(loader);
auto bestForLoader = bestMatch.value(loader);
// awkward case where the mod has only two loaders and one of them is not specified
if (loader != noLoader && bestMatch.contains(noLoader) && bestMatch.size() == 2) {
auto bestForNoLoader = bestMatch.value(noLoader);
if (bestForNoLoader.date > bestForLoader.date) {
return bestForNoLoader;
}
}
return bestForLoader;
}
}
return {};

View File

@ -105,9 +105,6 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> ModPlatform::IndexedVersion
{
auto versionArray = Json::requireArray(obj, "gameVersions");
if (versionArray.isEmpty()) {
return {};
}
ModPlatform::IndexedVersion file;
for (auto mcVer : versionArray) {

View File

@ -45,7 +45,7 @@ static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest)
Flame::File file;
loadFileV1(file, obj);
Q_ASSERT(file.projectId != 0);
pack.files.insert(file.fileId, file);
}

View File

@ -54,7 +54,7 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash,
if (mcVersions.has_value()) {
QStringList game_versions;
for (auto& ver : mcVersions.value()) {
game_versions.append(ver.toString());
game_versions.append(mapMCVersionToModrinth(ver));
}
Json::writeStringList(body_obj, "game_versions", game_versions);
}
@ -87,7 +87,7 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes,
if (mcVersions.has_value()) {
QStringList game_versions;
for (auto& ver : mcVersions.value()) {
game_versions.append(ver.toString());
game_versions.append(mapMCVersionToModrinth(ver));
}
Json::writeStringList(body_obj, "game_versions", game_versions);
}

View File

@ -81,6 +81,21 @@ class ModrinthAPI : public NetworkResourceAPI {
return {};
}
[[nodiscard]] static inline QString mapMCVersionFromModrinth(QString v)
{
static const QString preString = " Pre-Release ";
bool pre = false;
if (v.contains("-pre")) {
pre = true;
v.replace("-pre", preString);
}
v.replace("-", " ");
if (pre) {
v.replace(" Pre Release ", preString);
}
return v;
}
private:
[[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type)
{
@ -170,7 +185,7 @@ class ModrinthAPI : public NetworkResourceAPI {
{
QString s;
for (auto& ver : mcVersions) {
s += QString("\"versions:%1\",").arg(ver.toString());
s += QString("\"versions:%1\",").arg(mapMCVersionToModrinth(ver));
}
s.remove(s.length() - 1, 1); // remove last comma
return s.isEmpty() ? QString() : s;
@ -187,7 +202,7 @@ class ModrinthAPI : public NetworkResourceAPI {
: QString("%1/project/%2/version?game_versions=[\"%3\"]&loaders=[\"%4\"]")
.arg(BuildConfig.MODRINTH_PROD_URL)
.arg(args.dependency.addonId.toString())
.arg(args.mcVersion.toString())
.arg(mapMCVersionToModrinth(args.mcVersion))
.arg(getModLoaderStrings(args.loader).join("\",\""));
};
};

View File

@ -131,9 +131,7 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArra
pack.versionsLoaded = true;
}
auto Modrinth::loadIndexedPackVersion(QJsonObject& obj,
QString preferred_hash_type,
QString preferred_file_name) -> ModPlatform::IndexedVersion
ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_type, QString preferred_file_name)
{
ModPlatform::IndexedVersion file;
@ -145,7 +143,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj,
return {};
}
for (auto mcVer : versionArray) {
file.mcVersion.append(mcVer.toString());
file.mcVersion.append(ModrinthAPI::mapMCVersionFromModrinth(mcVer.toString()));
}
auto loaders = Json::requireArray(obj, "loaders");
for (auto loader : loaders) {
@ -247,9 +245,9 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj,
return {};
}
auto Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m,
QJsonArray& arr,
const BaseInstance* inst) -> ModPlatform::IndexedVersion
ModPlatform::IndexedVersion Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m,
QJsonArray& arr,
const BaseInstance* inst)
{
auto profile = (dynamic_cast<const MinecraftInstance*>(inst))->getPackProfile();
QString mcVersion = profile->getComponentVersion("net.minecraft");

View File

@ -40,9 +40,6 @@
#include "modplatform/modrinth/ModrinthAPI.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include <QSet>
static ModrinthAPI api;
@ -134,6 +131,7 @@ auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion
auto gameVersions = Json::ensureArray(obj, "game_versions");
if (!gameVersions.isEmpty()) {
file.gameVersion = Json::ensureString(gameVersions[0]);
file.gameVersion = ModrinthAPI::mapMCVersionFromModrinth(file.gameVersion);
}
auto loaders = Json::requireArray(obj, "loaders");
for (auto loader : loaders) {

View File

@ -54,7 +54,7 @@ Task::State FileSink::init(QNetworkRequest& request)
return Task::State::Failed;
}
wroteAnyData = false;
m_wroteAnyData = false;
m_output_file.reset(new PSaveFile(m_filename));
if (!m_output_file->open(QIODevice::WriteOnly)) {
qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing";
@ -72,17 +72,19 @@ Task::State FileSink::write(QByteArray& data)
qCCritical(taskNetLogC) << "Failed writing into " + m_filename;
m_output_file->cancelWriting();
m_output_file.reset();
wroteAnyData = false;
m_wroteAnyData = false;
return Task::State::Failed;
}
wroteAnyData = true;
m_wroteAnyData = true;
return Task::State::Running;
}
Task::State FileSink::abort()
{
m_output_file->cancelWriting();
if (m_output_file) {
m_output_file->cancelWriting();
}
failAllValidators();
return Task::State::Failed;
}
@ -100,7 +102,7 @@ Task::State FileSink::finalize(QNetworkReply& reply)
// if we wrote any data to the save file, we try to commit the data to the real file.
// if it actually got a proper file, we write it even if it was empty
if (gotFile || wroteAnyData) {
if (gotFile || m_wroteAnyData) {
// ask validators for data consistency
// we only do this for actual downloads, not 'your data is still the same' cache hits
if (!finalizeAllValidators(reply))

View File

@ -58,7 +58,7 @@ class FileSink : public Sink {
protected:
QString m_filename;
bool wroteAnyData = false;
bool m_wroteAnyData = false;
std::unique_ptr<PSaveFile> m_output_file;
};
} // namespace Net

View File

@ -78,7 +78,7 @@ Task::State MetaCacheSink::finalizeCache(QNetworkReply& reply)
{
QFileInfo output_file_info(m_filename);
if (wroteAnyData) {
if (m_wroteAnyData) {
m_entry->setMD5Sum(m_md5Node->hash().toHex().constData());
}

View File

@ -80,7 +80,7 @@ void NetRequest::executeTask()
emit finished();
return;
case State::Running:
qCDebug(logCat) << getUid().toString() << "Runninng " << m_url.toString();
qCDebug(logCat) << getUid().toString() << "Running " << m_url.toString();
break;
case State::Inactive:
case State::Failed:

View File

@ -52,6 +52,30 @@
#include <settings/SettingsObject.h>
#include "Application.h"
constexpr int MaxMclogsLines = 25000;
constexpr int InitialMclogsLines = 10000;
constexpr int FinalMclogsLines = 14900;
QString truncateLogForMclogs(const QString& logContent)
{
QStringList lines = logContent.split("\n");
if (lines.size() > MaxMclogsLines) {
QString truncatedLog = lines.mid(0, InitialMclogsLines).join("\n");
truncatedLog +=
"\n\n\n\n\n\n\n\n\n\n"
"------------------------------------------------------------\n"
"----------------------- Log truncated ----------------------\n"
"------------------------------------------------------------\n"
"----- Middle portion omitted to fit mclo.gs size limits ----\n"
"------------------------------------------------------------\n"
"\n\n\n\n\n\n\n\n\n\n";
truncatedLog += lines.mid(lines.size() - FinalMclogsLines - 1).join("\n");
return truncatedLog;
}
return logContent;
}
QString GuiUtil::fetchFlameKey(QWidget* parentWidget)
{
ProgressDialog prog(parentWidget);
@ -78,6 +102,7 @@ std::optional<QString> GuiUtil::uploadPaste(const QString& name, const QString&
ProgressDialog dialog(parentWidget);
auto pasteTypeSetting = static_cast<PasteUpload::PasteType>(APPLICATION->settings()->get("PastebinType").toInt());
auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString();
bool shouldTruncate = false;
{
QUrl baseUrl;
@ -97,10 +122,36 @@ std::optional<QString> GuiUtil::uploadPaste(const QString& name, const QString&
if (response != QMessageBox::Yes)
return {};
if (baseUrl.toString() == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) {
auto truncateResponse = CustomMessageBox::selectable(
parentWidget, QObject::tr("Confirm Truncation"),
QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n"
"The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n"
"If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off "
"potentially useful info like crashes at the end.\n\n"
"Proceed with truncation?")
.arg(text.count("\n"))
.arg(MaxMclogsLines)
.arg(InitialMclogsLines)
.arg(FinalMclogsLines),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No)
->exec();
if (truncateResponse == QMessageBox::Cancel) {
return {};
}
shouldTruncate = truncateResponse == QMessageBox::Yes;
}
}
}
std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, text, pasteCustomAPIBaseSetting, pasteTypeSetting));
QString textToUpload = text;
if (shouldTruncate) {
textToUpload = truncateLogForMclogs(text);
}
std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, textToUpload, pasteCustomAPIBaseSetting, pasteTypeSetting));
dialog.execWithTask(paste.get());
if (!paste->wasSuccessful()) {

View File

@ -979,6 +979,14 @@ void MainWindow::processURLs(QList<QUrl> urls)
continue;
}
if (APPLICATION->instances()->count() <= 0) {
CustomMessageBox::selectable(this, tr("No instance!"),
tr("No instance available to add the resource to.\nPlease create a new instance before "
"attempting to install this resource again."),
QMessageBox::Critical)
->show();
continue;
}
ImportResourceDialog dlg(localFileName, type, this);
if (dlg.exec() != QDialog::Accepted)

View File

@ -1,64 +0,0 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* 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
*
* http://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.
*/
#include "EditAccountDialog.h"
#include <DesktopServices.h>
#include <QPushButton>
#include <QUrl>
#include "ui_EditAccountDialog.h"
EditAccountDialog::EditAccountDialog(const QString& text, QWidget* parent, int flags) : QDialog(parent), ui(new Ui::EditAccountDialog)
{
ui->setupUi(this);
ui->label->setText(text);
ui->label->setVisible(!text.isEmpty());
ui->userTextBox->setEnabled(flags & UsernameField);
ui->passTextBox->setEnabled(flags & PasswordField);
ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
}
EditAccountDialog::~EditAccountDialog()
{
delete ui;
}
void EditAccountDialog::on_label_linkActivated(const QString& link)
{
DesktopServices::openUrl(QUrl(link));
}
void EditAccountDialog::setUsername(const QString& user) const
{
ui->userTextBox->setText(user);
}
QString EditAccountDialog::username() const
{
return ui->userTextBox->text();
}
void EditAccountDialog::setPassword(const QString& pass) const
{
ui->passTextBox->setText(pass);
}
QString EditAccountDialog::password() const
{
return ui->passTextBox->text();
}

View File

@ -1,52 +0,0 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* 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
*
* http://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.
*/
#pragma once
#include <QDialog>
namespace Ui {
class EditAccountDialog;
}
class EditAccountDialog : public QDialog {
Q_OBJECT
public:
explicit EditAccountDialog(const QString& text = "", QWidget* parent = 0, int flags = UsernameField | PasswordField);
~EditAccountDialog();
void setUsername(const QString& user) const;
void setPassword(const QString& pass) const;
QString username() const;
QString password() const;
enum Flags {
NoFlags = 0,
//! Specifies that the dialog should have a username field.
UsernameField,
//! Specifies that the dialog should have a password field.
PasswordField,
};
private slots:
void on_label_linkActivated(const QString& link);
private:
Ui::EditAccountDialog* ui;
};

View File

@ -1,94 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EditAccountDialog</class>
<widget class="QDialog" name="EditAccountDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>148</height>
</rect>
</property>
<property name="windowTitle">
<string>Login</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string notr="true">Message label placeholder.</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="userTextBox">
<property name="placeholderText">
<string>Email</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="passTextBox">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Password</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>EditAccountDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>EditAccountDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -30,6 +30,9 @@ Choose your name carefully:</string>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
<property name="buddy">
<cstring>nameEdit</cstring>
</property>

View File

@ -207,7 +207,7 @@
<item row="0" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Note: you only need to set this to access private data. Read the &lt;a href=&quot;https://docs.modrinth.com/#section/Authentication&quot;&gt;documentation&lt;/a&gt; for more information.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Note: you only need to set this to access private data. Read the &lt;a href=&quot;https://docs.modrinth.com/api/#authentication&quot;&gt;documentation&lt;/a&gt; for more information.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>

View File

@ -66,7 +66,7 @@ class AccountListPage : public QMainWindow, public BasePage {
return icon;
}
QString id() const override { return "accounts"; }
QString helpPage() const override { return "/getting-started/adding-an-account"; }
QString helpPage() const override { return "getting-started/adding-an-account"; }
void retranslate() override;
public slots:

View File

@ -93,6 +93,11 @@ InstanceSettingsPage::InstanceSettingsPage(BaseInstance* inst, QWidget* parent)
ui->serverJoinAddress->setEnabled(true);
ui->serverJoinAddressButton->setStyleSheet("QRadioButton::indicator { width: 0px; height: 0px; }");
}
connect(ui->javaPathTextBox, &QLineEdit::textChanged, [this](QString newValue) {
if (m_instance->settings()->get("JavaPath").toString() != newValue) {
m_instance->settings()->set("AutomaticJava", false);
}
});
loadSettings();

View File

@ -245,7 +245,6 @@ ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWin
}
// MODRINTH
void ModrinthManagedPackPage::parseManagedPack()
{
qDebug() << "Parsing Modrinth pack";
@ -338,6 +337,25 @@ void ModrinthManagedPackPage::suggestVersion()
ManagedPackPage::suggestVersion();
}
/// @brief Called when the update task has completed.
/// Internally handles the closing of the instance window if the update was successful and shows a message box.
/// @param did_succeed Whether the update task was successful.
void ManagedPackPage::onUpdateTaskCompleted(bool did_succeed) const
{
// Close the window if the update was successful
if (did_succeed) {
if (m_instance_window != nullptr)
m_instance_window->close();
CustomMessageBox::selectable(nullptr, tr("Update Successful"), tr("The instance updated to pack version %1 successfully.").arg(m_inst->getManagedPackVersionName()), QMessageBox::Information)
->show();
} else {
CustomMessageBox::selectable(nullptr, tr("Update Failed"), tr("The instance failed to update to pack version %1. Please check launcher logs for more information.").arg(m_inst->getManagedPackVersionName()), QMessageBox::Critical)
->show();
}
}
void ModrinthManagedPackPage::update()
{
auto index = ui->versionsComboBox->currentIndex();
@ -363,10 +381,9 @@ void ModrinthManagedPackPage::update()
extracted->setIcon(m_inst->iconKey());
extracted->setConfirmUpdate(false);
// Run our task then handle the result
auto did_succeed = runUpdateTask(extracted);
if (m_instance_window && did_succeed)
m_instance_window->close();
onUpdateTaskCompleted(did_succeed);
}
void ModrinthManagedPackPage::updateFromFile()
@ -386,14 +403,12 @@ void ModrinthManagedPackPage::updateFromFile()
extracted->setIcon(m_inst->iconKey());
extracted->setConfirmUpdate(false);
// Run our task then handle the result
auto did_succeed = runUpdateTask(extracted);
if (m_instance_window && did_succeed)
m_instance_window->close();
onUpdateTaskCompleted(did_succeed);
}
// FLAME
FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent)
: ManagedPackPage(inst, instance_window, parent)
{
@ -531,9 +546,7 @@ void FlameManagedPackPage::update()
extracted->setConfirmUpdate(false);
auto did_succeed = runUpdateTask(extracted);
if (m_instance_window && did_succeed)
m_instance_window->close();
onUpdateTaskCompleted(did_succeed);
}
void FlameManagedPackPage::updateFromFile()
@ -555,8 +568,6 @@ void FlameManagedPackPage::updateFromFile()
extracted->setConfirmUpdate(false);
auto did_succeed = runUpdateTask(extracted);
if (m_instance_window && did_succeed)
m_instance_window->close();
onUpdateTaskCompleted(did_succeed);
}
#include "ManagedPackPage.moc"

View File

@ -94,6 +94,8 @@ class ManagedPackPage : public QWidget, public BasePage {
BaseInstance* m_inst;
bool m_loaded = false;
void onUpdateTaskCompleted(bool did_succeed) const;
};
/** Simple page for when we aren't a managed pack. */

View File

@ -252,8 +252,11 @@ void VersionPage::updateButtons(int row)
bool VersionPage::reloadPackProfile()
{
try {
m_profile->reload(Net::Mode::Online);
return true;
auto result = m_profile->reload(Net::Mode::Online);
if (!result) {
QMessageBox::critical(this, tr("Error"), result.error);
}
return result;
} catch (const Exception& e) {
QMessageBox::critical(this, tr("Error"), e.cause());
return false;

View File

@ -40,18 +40,29 @@
#include "HintOverrideProxyStyle.h"
#include "ThemeManager.h"
SystemTheme::SystemTheme(const QString& styleName, const QPalette& palette, bool isDefaultTheme)
// See https://github.com/MultiMC/Launcher/issues/1790
// or https://github.com/PrismLauncher/PrismLauncher/issues/490
static const QStringList S_NATIVE_STYLES{ "windows11", "windowsvista", "macos", "system", "windows" };
SystemTheme::SystemTheme(const QString& styleName, const QPalette& defaultPalette, bool isDefaultTheme)
{
themeName = isDefaultTheme ? "system" : styleName;
widgetTheme = styleName;
colorPalette = palette;
m_themeName = isDefaultTheme ? "system" : styleName;
m_widgetTheme = styleName;
// NOTE: SystemTheme is reconstructed on page refresh. We can't accurately determine the system palette here
// See also S_NATIVE_STYLES comment
if (S_NATIVE_STYLES.contains(m_themeName)) {
m_colorPalette = defaultPalette;
} else {
auto style = QStyleFactory::create(styleName);
m_colorPalette = style->standardPalette();
delete style;
}
}
void SystemTheme::apply(bool initial)
{
// See https://github.com/MultiMC/Launcher/issues/1790
// or https://github.com/PrismLauncher/PrismLauncher/issues/490
if (initial) {
// See S_NATIVE_STYLES comment
if (initial && S_NATIVE_STYLES.contains(m_themeName)) {
QApplication::setStyle(new HintOverrideProxyStyle(QStyleFactory::create(qtTheme())));
return;
}
@ -61,35 +72,35 @@ void SystemTheme::apply(bool initial)
QString SystemTheme::id()
{
return themeName;
return m_themeName;
}
QString SystemTheme::name()
{
if (themeName.toLower() == "windowsvista") {
if (m_themeName.toLower() == "windowsvista") {
return QObject::tr("Windows Vista");
} else if (themeName.toLower() == "windows") {
} else if (m_themeName.toLower() == "windows") {
return QObject::tr("Windows 9x");
} else if (themeName.toLower() == "windows11") {
} else if (m_themeName.toLower() == "windows11") {
return QObject::tr("Windows 11");
} else if (themeName.toLower() == "system") {
} else if (m_themeName.toLower() == "system") {
return QObject::tr("System");
} else {
return themeName;
return m_themeName;
}
}
QString SystemTheme::tooltip()
{
if (themeName.toLower() == "windowsvista") {
if (m_themeName.toLower() == "windowsvista") {
return QObject::tr("Widget style trying to look like your win32 theme");
} else if (themeName.toLower() == "windows") {
} else if (m_themeName.toLower() == "windows") {
return QObject::tr("Windows 9x inspired widget style");
} else if (themeName.toLower() == "windows11") {
} else if (m_themeName.toLower() == "windows11") {
return QObject::tr("WinUI 3 inspired Qt widget style");
} else if (themeName.toLower() == "fusion") {
} else if (m_themeName.toLower() == "fusion") {
return QObject::tr("The default Qt widget style");
} else if (themeName.toLower() == "system") {
} else if (m_themeName.toLower() == "system") {
return QObject::tr("Your current system theme");
} else {
return "";
@ -98,12 +109,12 @@ QString SystemTheme::tooltip()
QString SystemTheme::qtTheme()
{
return widgetTheme;
return m_widgetTheme;
}
QPalette SystemTheme::colorScheme()
{
return colorPalette;
return m_colorPalette;
}
QString SystemTheme::appStyleSheet()

View File

@ -38,7 +38,7 @@
class SystemTheme : public ITheme {
public:
SystemTheme(const QString& styleName, const QPalette& palette, bool isDefaultTheme);
SystemTheme(const QString& styleName, const QPalette& defaultPalette, bool isDefaultTheme);
virtual ~SystemTheme() {}
void apply(bool initial) override;
@ -53,7 +53,7 @@ class SystemTheme : public ITheme {
QColor fadeColor() override;
private:
QPalette colorPalette;
QString widgetTheme;
QString themeName;
QPalette m_colorPalette;
QString m_widgetTheme;
QString m_themeName;
};

View File

@ -68,8 +68,8 @@ class ThemeManager {
QDir m_applicationThemeFolder{ "themes" };
QDir m_catPacksFolder{ "catpacks" };
std::map<QString, std::unique_ptr<CatPack>> m_catPacks;
QString m_defaultStyle;
QPalette m_defaultPalette;
QString m_defaultStyle;
LogColors m_logColors;
void initializeThemes();

View File

@ -40,7 +40,7 @@ class CheckComboModel : public QIdentityProxyModel {
{
if (role == Qt::CheckStateRole) {
auto txt = QIdentityProxyModel::data(index, Qt::DisplayRole).toString();
return checked.contains(txt) ? Qt::Checked : Qt::Unchecked;
return m_checked.contains(txt) ? Qt::Checked : Qt::Unchecked;
}
if (role == Qt::DisplayRole)
return QIdentityProxyModel::data(index, Qt::DisplayRole);
@ -50,10 +50,10 @@ class CheckComboModel : public QIdentityProxyModel {
{
if (role == Qt::CheckStateRole) {
auto txt = QIdentityProxyModel::data(index, Qt::DisplayRole).toString();
if (checked.contains(txt)) {
checked.removeOne(txt);
if (m_checked.contains(txt)) {
m_checked.removeOne(txt);
} else {
checked.push_back(txt);
m_checked.push_back(txt);
}
emit dataChanged(index, index);
emit checkStateChanged();
@ -61,13 +61,13 @@ class CheckComboModel : public QIdentityProxyModel {
}
return QIdentityProxyModel::setData(index, value, role);
}
QStringList getChecked() { return checked; }
QStringList getChecked() { return m_checked; }
signals:
void checkStateChanged();
private:
QStringList checked;
QStringList m_checked;
};
CheckComboBox::CheckComboBox(QWidget* parent) : QComboBox(parent), m_separator(", ")
@ -92,7 +92,7 @@ void CheckComboBox::setSourceModel(QAbstractItemModel* new_model)
void CheckComboBox::hidePopup()
{
if (!containerMousePress)
if (!m_containerMousePress)
QComboBox::hidePopup();
}
@ -138,7 +138,7 @@ bool CheckComboBox::eventFilter(QObject* receiver, QEvent* event)
}
case QEvent::MouseButtonPress: {
auto ev = static_cast<QMouseEvent*>(event);
containerMousePress = ev && view()->indexAt(ev->pos()).isValid();
m_containerMousePress = ev && view()->indexAt(ev->pos()).isValid();
break;
}
case QEvent::Wheel:

View File

@ -60,5 +60,5 @@ class CheckComboBox : public QComboBox {
private:
QString m_default_text;
QString m_separator;
bool containerMousePress;
bool m_containerMousePress = false;
};

View File

@ -47,6 +47,9 @@
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
@ -68,6 +71,9 @@
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>

View File

@ -38,9 +38,6 @@ Example:
# Note that this may break the reproducibility mentioned above, and you might not be able to access the binary cache
#
# inputs.nixpkgs.follows = "nixpkgs";
# This is not required for Flakes
inputs.flake-compat.follows = "";
};
};
@ -86,9 +83,6 @@ Example:
# Note that this may break the reproducibility mentioned above, and you might not be able to access the binary cache
#
# inputs.nixpkgs.follows = "nixpkgs";
# This is not required for Flakes
inputs.flake-compat.follows = "";
};
};

View File

@ -1,42 +0,0 @@
{
runCommand,
deadnix,
llvmPackages_18,
markdownlint-cli,
nixfmt-rfc-style,
statix,
self,
}:
{
formatting =
runCommand "check-formatting"
{
nativeBuildInputs = [
deadnix
llvmPackages_18.clang-tools
markdownlint-cli
nixfmt-rfc-style
statix
];
}
''
cd ${self}
echo "Running clang-format...."
clang-format --dry-run --style='file' --Werror */**.{c,cc,cpp,h,hh,hpp}
echo "Running deadnix..."
deadnix --fail
echo "Running markdownlint..."
markdownlint --dot .
echo "Running nixfmt..."
nixfmt --check .
echo "Running statix"
statix check .
touch $out
'';
}

View File

@ -3,7 +3,7 @@
stdenv,
cmake,
cmark,
apple-sdk_11,
darwin,
extra-cmake-modules,
gamemode,
ghc_filesystem,
@ -11,32 +11,54 @@
kdePackages,
libnbtplusplus,
ninja,
nix-filter,
self,
stripJavaArchivesHook,
tomlplusplus,
zlib,
msaClientID ? null,
gamemodeSupport ? stdenv.hostPlatform.isLinux,
}:
assert lib.assertMsg (
gamemodeSupport -> stdenv.hostPlatform.isLinux
) "gamemodeSupport is only available on Linux.";
let
date =
let
# YYYYMMDD
date' = lib.substring 0 8 self.lastModifiedDate;
year = lib.substring 0 4 date';
month = lib.substring 4 2 date';
date = lib.substring 6 2 date';
in
if (self ? "lastModifiedDate") then
lib.concatStringsSep "-" [
year
month
date
]
else
"unknown";
in
stdenv.mkDerivation {
pname = "shatteredprism-unwrapped";
version = self.shortRev or self.dirtyShortRev or "unknown";
version = "1.7-unstable-${date}";
src = nix-filter.lib {
root = self;
include = [
"buildconfig"
"cmake"
"launcher"
"libraries"
"program_info"
"tests"
../COPYING.md
src = lib.fileset.toSource {
root = ../.;
fileset = lib.fileset.unions [
../CMakeLists.txt
../COPYING.md
../buildconfig
../cmake
../launcher
../libraries
../program_info
../tests
];
};
@ -63,7 +85,7 @@ stdenv.mkDerivation {
tomlplusplus
zlib
]
++ lib.optionals stdenv.hostPlatform.isDarwin [ apple-sdk_11 ]
++ lib.optionals stdenv.hostPlatform.isDarwin [ darwin.apple_sdk.frameworks.Cocoa ]
++ lib.optional gamemodeSupport gamemode;
hardeningEnable = lib.optionals stdenv.hostPlatform.isLinux [ "pie" ];